BattleMoveAccuracyEvasionPhaseStep.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.BattleStatIds
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
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.BattleStatStageSupport

/**
 * 命中与回避修正 phase step。
 *
 * @property phaseProcessor battle hook phase 处理器。
 */
class BattleMoveAccuracyEvasionPhaseStep(
    private val phaseProcessor: BattleFlowPhaseProcessor,
) : BattleMoveResolutionStep {
    /**
     * 当前步骤在 pipeline 中的执行顺序。
     */
    override val order: Int = 100

    /**
     * 执行命中与回避修正阶段。
     */
    override fun execute(context: BattleMoveResolutionContext) {
        if (context.forcedHitSuccessful != null || context.skipAccuracyEvasionModifiers) {
            return
        }
        val attacker = context.snapshot.units[context.attackerId]
        val target = context.snapshot.units[context.targetId]
        val attackerSide = sideOfUnit(context.snapshot, context.attackerId)
        val accuracyPhaseAttributes =
            context.attributes +
                mapOf(
                    BattleAttributeKeys.MOVE_TYPE to BattleMoveDataReader.readType(context.moveEffect.data),
                    BattleAttributeKeys.DAMAGE_CLASS to BattleMoveDataReader.readDamageClass(context.moveEffect.data),
                )
        val attackerAccuracyStage = BattleStatStageSupport.readStage(attacker?.boosts.orEmpty(), BattleStatIds.ACCURACY)
        val targetEvasionStage =
            if (attacker?.abilityId == KEEN_EYE_ABILITY_ID) {
                0
            } else {
                BattleStatStageSupport.readStage(target?.boosts.orEmpty(), BattleStatIds.EVASION)
            }
        val hasVictoryStarAura =
            attackerSide
                ?.activeUnitIds
                .orEmpty()
                .mapNotNull { unitId -> context.snapshot.units[unitId]?.abilityId }
                .any { abilityId -> abilityId == VICTORY_STAR_ABILITY_ID }
        context.accuracy =
            context.accuracy
                ?.let { accuracy ->
                    var adjustedAccuracy = accuracy * BattleStatStageSupport.accuracyEvasionMultiplier(attackerAccuracyStage)
                    if (hasVictoryStarAura) {
                        adjustedAccuracy *= VICTORY_STAR_MULTIPLIER
                    }
                    if (BattleFieldConditionSupport.hasGravity(context.snapshot)) {
                        adjustedAccuracy *= GRAVITY_ACCURACY_MULTIPLIER
                    }
                    adjustedAccuracy.toInt()
                }
        context.evasion =
            context.evasion
                ?.let { evasion ->
                    (evasion * BattleStatStageSupport.accuracyEvasionMultiplier(targetEvasionStage)).toInt()
                }
        val accuracyResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_ACCURACY.value,
                moveEffect = context.moveEffect,
                selfId = context.attackerId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = context.accuracy?.toDouble(),
                attributes = accuracyPhaseAttributes,
            )
        context.snapshot = accuracyResult.snapshot
        context.accuracy = BattleRelayReader.readInt(accuracyResult.relay) ?: context.accuracy

        val evasionResult =
            phaseProcessor.processPhase(
                snapshot = context.snapshot,
                hookName = StandardHookNames.ON_MODIFY_EVASION.value,
                moveEffect = context.moveEffect,
                selfId = context.targetId,
                targetId = context.targetId,
                sourceId = context.sourceId,
                relay = context.evasion?.toDouble(),
                attributes = accuracyPhaseAttributes,
            )
        context.snapshot = evasionResult.snapshot
        context.evasion = BattleRelayReader.readInt(evasionResult.relay) ?: context.evasion
    }

    /**
     * 根据单位标识反查其所属 side。
     *
     * 命中阶段只需要读当前 active 队友能力,因此在本步骤内保留一份轻量查询即可,
     * 避免把整套 session 依赖引进到 flow 层。
     */
    private fun sideOfUnit(
        snapshot: BattleRuntimeSnapshot,
        unitId: String,
    ) = snapshot.sides.values.firstOrNull { side -> unitId in side.activeUnitIds || unitId in side.unitIds }

    private companion object {
        private const val KEEN_EYE_ABILITY_ID: String = "keen-eye"
        private const val VICTORY_STAR_ABILITY_ID: String = "victory-star"
        private const val VICTORY_STAR_MULTIPLIER: Double = 1.1
        private const val GRAVITY_ACCURACY_MULTIPLIER: Double = 5.0 / 3.0
    }
}