BattleMovePowerDamagePhaseStep.kt

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

import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleAttributeKeys
import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleDamageClassIds
import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleStatIds
import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleStatusIds
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleFieldConditionSupport
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleMoveDataReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleRelayReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatAliasResolver
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatStageSupport
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStoredBoostSupport
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.EventContextAttributeReader
import kotlin.math.floor

/**
 * 威力与伤害修正 phase step。
 *
 * @property phaseProcessor battle hook phase 处理器。
 */
class BattleMovePowerDamagePhaseStep(
    private val phaseProcessor: BattleFlowPhaseProcessor,
    private val typeEffectivenessResolver: BattleTypeEffectivenessResolver = NoopBattleTypeEffectivenessResolver,
    private val fixedDamageRuleResolver: BattleMoveFixedDamageRuleResolver = BattleMoveFixedDamageRuleResolver(),
    private val targetCountDamageModifier: BattleMoveTargetCountDamageModifier = BattleMoveTargetCountDamageModifier(),
) : BattleMoveResolutionStep {
    override val order: Int = 300

    override fun execute(context: BattleMoveResolutionContext) {
        val moveType = BattleMoveDataReader.readType(context.moveEffect.data)
        val damageClass = BattleMoveDataReader.readDamageClass(context.moveEffect.data)
        context.basePower =
            resolveDynamicBasePower(
                moveData = context.moveEffect.data,
                attacker = context.snapshot.units[context.attackerId],
                fallbackBasePower = context.basePower,
            )
        val basePowerResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_BASE_POWER.value,
                moveEffect = context.moveEffect,
                selfId = context.attackerId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = context.basePower.toDouble(),
                attributes =
                    context.attributes +
                        mapOf(
                            BattleAttributeKeys.MOVE_TYPE to moveType,
                            BattleAttributeKeys.DAMAGE_CLASS to damageClass,
                        ),
            )
        context.snapshot = basePowerResult.snapshot
        context.basePower = BattleRelayReader.readInt(basePowerResult.relay) ?: context.basePower

        val attacker = context.snapshot.units[context.attackerId]
        val target = context.snapshot.units[context.targetId]
        val typeMultiplier = typeEffectivenessResolver.resolve(moveType, attacker, target)
        val fixedDamage = fixedDamageRuleResolver.resolve(context.moveEffect.data, attacker, target)
        val usesFixedDamageRule = fixedDamage != null
        val shouldComputeNativeDamage = shouldComputeDamage(context, damageClass)
        if (usesFixedDamageRule) {
            context.damage = requireNotNull(fixedDamage)
        } else if (shouldComputeNativeDamage) {
            computeBaseDamage(context, moveType, damageClass, attacker, target)?.let { computedDamage ->
                context.damage = computedDamage
            }
        }

        if (!usesFixedDamageRule && typeMultiplier > 0.0) {
            context.damage =
                targetCountDamageModifier.apply(
                    damage = context.damage,
                    moveData = context.moveEffect.data,
                    attributes = context.attributes,
                )
        }

        if (usesFixedDamageRule) {
            if (typeMultiplier <= 0.0) {
                context.damage = 0
                context.hitSuccessful = false
            }
            return
        }

        val damageResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_DAMAGE.value,
                moveEffect = context.moveEffect,
                selfId = context.attackerId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = context.damage.toDouble(),
                attributes =
                    context.attributes +
                        mapOf(
                            BattleAttributeKeys.CRITICAL_HIT to context.criticalHit,
                            BattleAttributeKeys.MOVE_TYPE to moveType,
                            BattleAttributeKeys.DAMAGE_CLASS to damageClass,
                            BattleAttributeKeys.TYPE_MULTIPLIER to typeMultiplier,
                        ),
            )
        context.snapshot = damageResult.snapshot
        context.damage = BattleRelayReader.readInt(damageResult.relay) ?: context.damage

        if (context.criticalHit) {
            context.damage = floor(context.damage * CRITICAL_DAMAGE_MULTIPLIER).toInt()
        }
        if (shouldApplyDamageVariance(context, damageClass, shouldComputeNativeDamage) && typeMultiplier > 0.0) {
            val damageRoll = resolveDamageRoll(context)
            context.damageRoll = damageRoll
            context.damage = floor(context.damage * damageRoll / DAMAGE_ROLL_DENOMINATOR).toInt()
        }

        val stabMatched =
            context.basePower > 0 &&
                damageClass != null &&
                damageClass != BattleDamageClassIds.STATUS &&
                moveType != null &&
                attacker != null &&
                moveType in attacker.typeIds
        val baseStab = if (stabMatched) 1.5 else 1.0
        val stabResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_STAB.value,
                moveEffect = context.moveEffect,
                selfId = context.attackerId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = baseStab,
                attributes =
                    context.attributes +
                        mapOf(
                            BattleAttributeKeys.MOVE_TYPE to moveType,
                            BattleAttributeKeys.STAB_MATCHED to stabMatched,
                        ),
            )
        context.snapshot = stabResult.snapshot
        val stab = BattleRelayReader.readDouble(stabResult.relay) ?: baseStab
        context.damage = floor(context.damage * stab).toInt()

        if (typeMultiplier <= 0.0) {
            context.damage = 0
            context.hitSuccessful = false
            return
        }
        context.damage = floor(context.damage * typeMultiplier).toInt()
        if (context.basePower > 0 && context.damage <= 0) {
            context.damage = 1
        }
    }

    private fun shouldComputeDamage(
        context: BattleMoveResolutionContext,
        damageClass: String?,
    ): Boolean {
        if (context.basePower <= 0 || damageClass == null || damageClass == BattleDamageClassIds.STATUS) {
            return false
        }
        if (EventContextAttributeReader.readBoolean(BattleAttributeKeys.COMPUTE_DAMAGE, context.attributes) == true) {
            return true
        }
        return context.damage <= 0
    }

    private fun computeBaseDamage(
        context: BattleMoveResolutionContext,
        moveType: String?,
        damageClass: String?,
        attacker: UnitState?,
        target: UnitState?,
    ): Int? {
        if (attacker == null || target == null || damageClass == null) {
            return null
        }
        val attackStatKey = if (damageClass == BattleDamageClassIds.SPECIAL) BattleStatIds.SPECIAL_ATTACK else BattleStatIds.ATTACK
        val defenseStatKey = resolveDefenseStatKey(context, damageClass)
        val attackStat =
            resolveModifiedBattleStat(
                context = context,
                unit = attacker,
                statKey = attackStatKey,
                hookName = StandardHookNames.ON_MODIFY_ATTACK.value,
                moveType = moveType,
                damageClass = damageClass,
                stage = normalizedOffensiveStage(attacker, attackStatKey, context.criticalHit),
            ) ?: return null
        val defenseStat =
            resolveModifiedBattleStat(
                context = context,
                unit = target,
                statKey = defenseStatKey,
                hookName = StandardHookNames.ON_MODIFY_DEFENSE.value,
                moveType = moveType,
                damageClass = damageClass,
                stage =
                    normalizedDefensiveStage(
                        unit = target,
                        statKey = defenseStatKey,
                        criticalHit = context.criticalHit,
                        ignoreBoosts = BattleMoveDataReader.readIgnoreTargetDefensiveBoosts(context.moveEffect.data) == true,
                    ),
            ) ?: return null
        val burnedAttack =
            if (damageClass == BattleDamageClassIds.PHYSICAL && attacker.statusState?.effectId == BattleStatusIds.BURN && attacker.abilityId != GUTS_ABILITY) {
                attackStat * BURN_ATTACK_MULTIPLIER
            } else {
                attackStat
            }
        val level = attacker.metadata.level ?: DEFAULT_LEVEL
        val rawDamage = (((((2.0 * level) / 5.0) + 2.0) * context.basePower * burnedAttack / defenseStat) / 50.0) + 2.0
        return floor(rawDamage).toInt().coerceAtLeast(1)
    }

    private fun shouldApplyDamageVariance(
        context: BattleMoveResolutionContext,
        damageClass: String?,
        shouldComputeNativeDamage: Boolean,
    ): Boolean {
        if (context.basePower <= 0 || damageClass == null || damageClass == BattleDamageClassIds.STATUS) {
            return false
        }
        if (EventContextAttributeReader.readInt(BattleAttributeKeys.DAMAGE_ROLL, context.attributes) != null) {
            return true
        }
        return shouldComputeNativeDamage
    }

    private fun resolveDamageRoll(context: BattleMoveResolutionContext): Int {
        val explicitRoll = EventContextAttributeReader.readInt(BattleAttributeKeys.DAMAGE_ROLL, context.attributes)
        if (explicitRoll != null) {
            return explicitRoll.coerceIn(MIN_DAMAGE_ROLL, MAX_DAMAGE_ROLL)
        }
        val randomResult =
            context.snapshot.battle.randomState
                .nextInt(DAMAGE_ROLL_BOUND)
        context.snapshot =
            context.snapshot.copy(
                battle =
                    context.snapshot.battle.copy(
                        randomState = randomResult.nextState,
                    ),
            )
        return MIN_DAMAGE_ROLL + randomResult.value
    }

    private fun resolveModifiedBattleStat(
        context: BattleMoveResolutionContext,
        unit: UnitState,
        statKey: String,
        hookName: String,
        moveType: String?,
        damageClass: String,
        stage: Int,
    ): Double? {
        val baseStat = resolveStat(unit, statKey) ?: return null
        val stagedStat = baseStat * BattleStatStageSupport.stageMultiplier(stage)
        val hookResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = hookName,
                moveEffect = context.moveEffect,
                selfId = unit.id,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = stagedStat,
                attributes =
                    context.attributes +
                        mapOf(
                            BattleAttributeKeys.MOVE_TYPE to moveType,
                            BattleAttributeKeys.DAMAGE_CLASS to damageClass,
                            BattleAttributeKeys.CRITICAL_HIT to context.criticalHit,
                            BattleAttributeKeys.STAT_KEY to statKey,
                        ),
            )
        context.snapshot = hookResult.snapshot
        return (BattleRelayReader.readDouble(hookResult.relay) ?: stagedStat).coerceAtLeast(1.0)
    }

    private fun normalizedOffensiveStage(
        unit: UnitState,
        statKey: String,
        criticalHit: Boolean,
    ): Int {
        val stage = resolveBoost(unit, statKey)
        return if (criticalHit && stage < 0) 0 else stage
    }

    private fun normalizedDefensiveStage(
        unit: UnitState,
        statKey: String,
        criticalHit: Boolean,
        ignoreBoosts: Boolean = false,
    ): Int {
        if (ignoreBoosts) {
            return 0
        }
        val stage = resolveBoost(unit, statKey)
        return if (criticalHit && stage > 0) 0 else stage
    }

    private fun resolveDynamicBasePower(
        moveData: Map<String, Any?>,
        attacker: UnitState?,
        fallbackBasePower: Int,
    ): Int {
        val base = BattleMoveDataReader.readPositiveStageBasePowerBase(moveData)
        val perStage = BattleMoveDataReader.readBasePowerPerPositiveStage(moveData)
        if (base != null && perStage != null && attacker != null) {
            val positiveStages = BattleStoredBoostSupport.sumPositiveStages(attacker.boosts)
            return (base + positiveStages * perStage).coerceAtLeast(0)
        }
        return fallbackBasePower
    }

    private fun resolveStat(
        unit: UnitState,
        statKey: String,
    ): Int? = BattleStatAliasResolver.readValue(unit.stats, statKey)

    private fun resolveBoost(
        unit: UnitState,
        statKey: String,
    ): Int = BattleStatStageSupport.readStage(unit.boosts, statKey)

    /**
     * `Wonder Room` 生效期间,物防与特防在伤害公式中互换使用。
     *
     * 当前只交换 battle 计算口径,不改单位展示层原始 stat 存储,
     * 这样可以把规则影响限定在真实伤害结算链。
     */
    private fun resolveDefenseStatKey(
        context: BattleMoveResolutionContext,
        damageClass: String,
    ): String {
        if (!BattleFieldConditionSupport.hasWonderRoom(context.snapshot)) {
            return if (damageClass == BattleDamageClassIds.SPECIAL) BattleStatIds.SPECIAL_DEFENSE else BattleStatIds.DEFENSE
        }
        return if (damageClass == BattleDamageClassIds.SPECIAL) BattleStatIds.DEFENSE else BattleStatIds.SPECIAL_DEFENSE
    }

    private companion object {
        private const val DEFAULT_LEVEL: Int = 50
        private const val GUTS_ABILITY: String = "guts"
        private const val BURN_ATTACK_MULTIPLIER: Double = 0.5
        private const val CRITICAL_DAMAGE_MULTIPLIER: Double = 1.5
        private const val MIN_DAMAGE_ROLL: Int = 85
        private const val MAX_DAMAGE_ROLL: Int = 100
        private const val DAMAGE_ROLL_BOUND: Int = 16
        private const val DAMAGE_ROLL_DENOMINATOR: Double = 100.0
    }
}