BattleUnitAssembler.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitDebugState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitMetadataState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.calculator.growthrate.GrowthRateCalculatorFactory
import io.github.lishangbu.avalon.game.calculator.stat.StatCalculatorFactory

data class CreatureAbilityOptionRecord(
    val internalName: String,
    val slot: Int,
    val hidden: Boolean,
)

data class CreatureUnitImportRecord(
    val id: Long,
    val speciesId: Long?,
    val internalName: String,
    val name: String,
    val weight: Int?,
    val growthRateInternalName: String?,
    val captureRate: Int?,
    val typeIds: List<String>,
    val baseStats: Map<String, Int>,
    val abilityOptions: List<CreatureAbilityOptionRecord>,
)

data class NatureImportRecord(
    val id: Long?,
    val internalName: String,
    val increasedStatInternalName: String?,
    val decreasedStatInternalName: String?,
)

data class BattleMoveSlotInput(
    val moveId: String,
    val currentPp: Int? = null,
)

data class BattleUnitAssemblyRequest(
    val unitId: String,
    val metadata: UnitMetadataState,
    val abilityInternalName: String? = null,
    val itemId: String? = null,
    val moves: List<BattleMoveSlotInput> = emptyList(),
    val currentHp: Int? = null,
    val statusState: AttachedEffectState? = null,
    val volatileStates: Map<String, AttachedEffectState> = emptyMap(),
    val conditionStates: Map<String, AttachedEffectState> = emptyMap(),
    val boosts: Map<String, Int> = emptyMap(),
    val debugState: UnitDebugState = UnitDebugState(),
    val forceSwitchRequested: Boolean = false,
)

data class BattleUnitAssemblyResult(
    val unit: UnitState,
    val creatureId: Long,
    val creatureInternalName: String,
    val creatureName: String,
    val level: Int,
    val requiredExperience: Int,
    val calculatedStats: Map<String, Int>,
)

object BattleUnitAssembler {
    private const val DEFAULT_IV: Int = 31
    private const val DEFAULT_EV: Int = 0
    private const val NEUTRAL_NATURE: Int = 100
    private const val INCREASED_NATURE: Int = 110
    private const val DECREASED_NATURE: Int = 90

    fun assemble(
        request: BattleUnitAssemblyRequest,
        creature: CreatureUnitImportRecord,
        nature: NatureImportRecord?,
        movePpDefaults: Map<String, Int>,
        statCalculatorFactory: StatCalculatorFactory,
        growthRateCalculatorFactory: GrowthRateCalculatorFactory,
    ): BattleUnitAssemblyResult {
        val level = requireNotNull(request.metadata.level) { "Battle unit metadata.level must not be null." }
        require(level in 1..100) { "Battle unit level must be between 1 and 100." }

        val calculatedStats =
            creature.baseStats.mapValues { (statInternalName, baseStat) ->
                statCalculatorFactory.calculateStat(
                    internalName = statInternalName,
                    base = baseStat,
                    dv = request.metadata.ivs[statInternalName] ?: DEFAULT_IV,
                    stateExp = request.metadata.evs[statInternalName] ?: DEFAULT_EV,
                    level = level,
                    nature = natureModifier(statInternalName, nature),
                )
            }
        val maxHp = (calculatedStats["hp"] ?: 0).coerceAtLeast(1)
        val currentHp = (request.currentHp ?: maxHp).coerceIn(0, maxHp)
        val selectedAbilityId = selectAbilityId(request.abilityInternalName, creature.abilityOptions)
        val requiredExperience =
            growthRateCalculatorFactory.calculateGrowthRate(
                internalName = creature.growthRateInternalName.orEmpty(),
                level = level,
            )
        return BattleUnitAssemblyResult(
            unit =
                UnitState(
                    id = request.unitId,
                    currentHp = currentHp,
                    maxHp = maxHp,
                    statusState = request.statusState,
                    abilityId = selectedAbilityId,
                    itemId = request.itemId,
                    typeIds = creature.typeIds.toSet(),
                    volatileStates = normalizeStateMap(request.volatileStates),
                    conditionStates = normalizeStateMap(request.conditionStates),
                    boosts = request.boosts,
                    stats = calculatedStats,
                    movePp =
                        request.moves.associate { slot ->
                            val defaultPp = movePpDefaults[slot.moveId] ?: 0
                            slot.moveId to (slot.currentPp ?: defaultPp)
                        },
                    metadata =
                        UnitMetadataState(
                            creatureId = creature.id,
                            creatureSpeciesId = creature.speciesId,
                            creatureInternalName = creature.internalName,
                            creatureName = creature.name,
                            level = level,
                            requiredExperience = requiredExperience,
                            natureId = nature?.id,
                            captureRate = creature.captureRate,
                            weight = creature.weight,
                            captureContext = request.metadata.captureContext,
                            ivs = request.metadata.ivs,
                            evs = request.metadata.evs,
                        ),
                    debugState = request.debugState,
                    forceSwitchRequested = request.forceSwitchRequested,
                ),
            creatureId = creature.id,
            creatureInternalName = creature.internalName,
            creatureName = creature.name,
            level = level,
            requiredExperience = requiredExperience,
            calculatedStats = calculatedStats,
        )
    }

    private fun natureModifier(
        statInternalName: String,
        nature: NatureImportRecord?,
    ): Int =
        when (statInternalName) {
            nature?.increasedStatInternalName -> INCREASED_NATURE
            nature?.decreasedStatInternalName -> DECREASED_NATURE
            else -> NEUTRAL_NATURE
        }

    private fun selectAbilityId(
        requestedAbilityInternalName: String?,
        abilityOptions: List<CreatureAbilityOptionRecord>,
    ): String? {
        if (requestedAbilityInternalName != null) {
            return requireNotNull(
                abilityOptions.firstOrNull { option -> option.internalName == requestedAbilityInternalName }?.internalName,
            ) {
                "Creature does not provide ability '$requestedAbilityInternalName'."
            }
        }

        return abilityOptions
            .sortedWith(compareBy<CreatureAbilityOptionRecord> { option -> option.hidden }.thenBy { option -> option.slot })
            .firstOrNull()
            ?.internalName
    }

    /**
     * 规范化导入入口传入的挂载效果状态表。
     *
     * 设计意图:
     * - 以 `AttachedEffectState.effectId` 为唯一真值来源,不信任外层 map key;
     * - 按 `effectOrder` 固定遍历顺序,避免导入结果受 map 实现细节影响;
     * - 导入边界直接接收完整 runtime state,不再做额外字符串镜像提升。
     */
    private fun normalizeStateMap(states: Map<String, AttachedEffectState>): Map<String, AttachedEffectState> =
        states.values
            .sortedBy(AttachedEffectState::effectOrder)
            .associateBy(AttachedEffectState::effectId)
}