BattleStoredBoostSupport.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleBoostRelaySelectionValues

/**
 * 既有 stored boosts 的读取、筛选与迁移支持组件。
 *
 * 设计意图:
 * - 把 `psych-up / power-swap / guard-swap / heart-swap / topsy-turvy / spectral-thief`
 *   这类基于“当前已持有能力阶级”的规则收口到统一 helper;
 * - 避免多个 action executor 各自重复实现 boost map 归一、过滤、交换和偷取逻辑;
 * - 保持所有 stored boost 改写都共享同一套别名、上下限和 0 值清理规则。
 */
object BattleStoredBoostSupport {
    /**
     * 一次 boost 转移的结果。
     *
     * @property sourceBoosts 转移后来源单位的 boosts。
     * @property targetBoosts 转移后目标单位的 boosts。
     * @property transferredBoosts 本次真正被转移的 boost 子集。
     */
    data class TransferResult(
        val sourceBoosts: Map<String, Int>,
        val targetBoosts: Map<String, Int>,
        val transferredBoosts: Map<String, Int>,
    )

    /**
     * 读取并规范化一份 stored boosts。
     */
    fun normalize(boosts: Map<String, Int>): Map<String, Int> = BattleStatStageSupport.normalizeStoredBoosts(boosts)

    /**
     * 按 selection 与 stats 子集筛选 stored boosts。
     */
    fun select(
        boosts: Map<String, Int>,
        selection: String = BattleBoostRelaySelectionValues.ALL,
        stats: Set<String> = emptySet(),
    ): Map<String, Int> {
        val normalizedBoosts = normalize(boosts)
        val canonicalStats =
            stats
                .map(BattleStatAliasResolver::canonicalBoostId)
                .toSet()
        return normalizedBoosts.filter { (statId, stage) ->
            (canonicalStats.isEmpty() || statId in canonicalStats) &&
                matchesSelection(stage, selection)
        }
    }

    /**
     * 用 replacement 替换 current 中指定 stat 子集的 boosts。
     */
    fun replaceSelected(
        current: Map<String, Int>,
        replacement: Map<String, Int>,
        selectedStats: Set<String>,
    ): Map<String, Int> {
        val normalizedCurrent = normalize(current)
        val normalizedReplacement = normalize(replacement)
        val canonicalStats =
            selectedStats
                .map(BattleStatAliasResolver::canonicalBoostId)
                .toSet()
        if (canonicalStats.isEmpty()) {
            return normalizedCurrent
        }
        val nextBoosts = normalizedCurrent.toMutableMap()
        canonicalStats.forEach(nextBoosts::remove)
        normalizedReplacement
            .filterKeys { statId -> statId in canonicalStats }
            .forEach { (statId, stage) ->
                nextBoosts[statId] = stage
            }
        return normalize(nextBoosts)
    }

    /**
     * 把指定 stat 子集取反。
     */
    fun invert(
        boosts: Map<String, Int>,
        stats: Set<String> = emptySet(),
    ): Map<String, Int> {
        val normalizedBoosts = normalize(boosts)
        val selectedStats = resolveSelectedStats(normalizedBoosts, emptyMap(), stats)
        val invertedSelection =
            selectedStats.associateWith { statId ->
                -(normalizedBoosts[statId] ?: 0)
            }
        return replaceSelected(normalizedBoosts, invertedSelection, selectedStats)
    }

    /**
     * 交换两份 boosts 的指定 stat 子集。
     */
    fun swap(
        left: Map<String, Int>,
        right: Map<String, Int>,
        stats: Set<String> = emptySet(),
    ): Pair<Map<String, Int>, Map<String, Int>> {
        val normalizedLeft = normalize(left)
        val normalizedRight = normalize(right)
        val selectedStats = resolveSelectedStats(normalizedLeft, normalizedRight, stats)
        val leftSelected = normalizedLeft.filterKeys { statId -> statId in selectedStats }
        val rightSelected = normalizedRight.filterKeys { statId -> statId in selectedStats }
        return replaceSelected(normalizedLeft, rightSelected, selectedStats) to
            replaceSelected(normalizedRight, leftSelected, selectedStats)
    }

    /**
     * 从 source 中取出指定 boosts,并把它们加到 target 身上。
     *
     * 当前主要用于 Spectral Thief 这类“偷取对方正向强化”的规则。
     */
    fun steal(
        source: Map<String, Int>,
        target: Map<String, Int>,
        selection: String = BattleBoostRelaySelectionValues.POSITIVE,
        stats: Set<String> = emptySet(),
    ): TransferResult {
        val normalizedSource = normalize(source)
        val transferredBoosts = select(normalizedSource, selection, stats)
        val sourceNext =
            normalizedSource.filterKeys { statId ->
                statId !in transferredBoosts.keys
            }
        val targetNext =
            BattleStatStageSupport.applyBoostDeltas(
                currentBoosts = normalize(target),
                deltas = transferredBoosts,
            )
        return TransferResult(
            sourceBoosts = normalize(sourceNext),
            targetBoosts = targetNext,
            transferredBoosts = transferredBoosts,
        )
    }

    /**
     * 统计当前 boosts 中全部正向 stage 的总和。
     */
    fun sumPositiveStages(boosts: Map<String, Int>): Int = normalize(boosts).values.filter { stage -> stage > 0 }.sum()

    private fun resolveSelectedStats(
        left: Map<String, Int>,
        right: Map<String, Int>,
        stats: Set<String>,
    ): Set<String> =
        if (stats.isNotEmpty()) {
            stats
                .map(BattleStatAliasResolver::canonicalBoostId)
                .toSet()
        } else {
            (left.keys + right.keys).toSet()
        }

    private fun matchesSelection(
        stage: Int,
        selection: String,
    ): Boolean =
        when (selection) {
            BattleBoostRelaySelectionValues.ALL -> true
            BattleBoostRelaySelectionValues.POSITIVE -> stage > 0
            BattleBoostRelaySelectionValues.NEGATIVE -> stage < 0
            else -> false
        }
}