DefaultBattleFlowPhaseProcessor.kt
package io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow
import io.github.lishangbu.avalon.game.battle.engine.core.dsl.EffectDefinition
import io.github.lishangbu.avalon.game.battle.engine.core.event.EventContext
import io.github.lishangbu.avalon.game.battle.engine.core.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.HookRuleProcessor
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplier
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleHeldItemRuntimeSupport
import io.github.lishangbu.avalon.game.battle.engine.spi.effect.EffectDefinitionRepository
/**
* 默认 battle hook phase 处理器。
*
* @property effectRepository effect 定义查询入口。
* @property hookRuleProcessor 单条 hook rule 处理器。
* @property mutationApplier mutation 写回组件。
* @property mutationInterceptorChain mutation 拦截链。
*/
class DefaultBattleFlowPhaseProcessor(
private val effectRepository: EffectDefinitionRepository,
private val hookRuleProcessor: HookRuleProcessor,
private val mutationApplier: MutationApplier,
private val mutationInterceptorChain: BattleMutationInterceptorChain,
) : BattleFlowPhaseProcessor {
private val triggeredHookDispatcher =
BattleTriggeredHookDispatcher(
attachedEffectProcessor =
BattleAttachedEffectProcessor { attachedSnapshot, unitId, hookName, attachedTargetId, attachedSourceId, relay, attributes ->
processAttachedEffects(
snapshot = attachedSnapshot,
unitId = unitId,
hookName = hookName,
targetId = attachedTargetId,
sourceId = attachedSourceId,
relay = relay,
attributes = attributes,
)
},
)
/**
* 处理一次完整的 hook phase。
*/
override fun processPhase(
snapshot: BattleRuntimeSnapshot,
hookName: String,
moveEffect: EffectDefinition,
selfId: String,
targetId: String,
sourceId: String,
relay: Any?,
attributes: Map<String, Any?>,
): HookPhaseResult {
var currentResult =
processEffectHook(
snapshot = snapshot,
hookName = hookName,
effect = moveEffect,
selfId = selfId,
targetId = targetId,
sourceId = sourceId,
relay = relay,
attributes = attributes,
)
if (currentResult.cancelled) {
return currentResult
}
val attachmentOrder =
attachmentOrderForPhase(
hookName = hookName,
selfId = selfId,
targetId = targetId,
)
attachmentOrder.forEach { unitId ->
currentResult =
processAttachedEffects(
snapshot = currentResult.snapshot,
unitId = unitId,
hookName = hookName,
targetId = targetId,
sourceId = sourceId,
relay = currentResult.relay,
attributes = attributes,
)
if (currentResult.cancelled) {
return currentResult
}
}
return currentResult
}
/**
* 处理某个单位上挂载 effect 的指定 hook。
*/
override fun processAttachedEffects(
snapshot: BattleRuntimeSnapshot,
unitId: String,
hookName: String,
targetId: String?,
sourceId: String?,
relay: Any?,
attributes: Map<String, Any?>,
): HookPhaseResult {
var currentResult = HookPhaseResult(snapshot = snapshot, cancelled = false, relay = relay)
val unit = requireUnit(currentResult.snapshot, unitId)
val unitSide = sideOfUnit(currentResult.snapshot, unitId)
val unitConditionEffectIds =
unit.conditionStates
.values
.sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
.map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
val unitVolatileEffectIds =
unit.volatileStates
.values
.sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
.map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
val sideConditionEffectIds =
buildList {
unitSide
?.conditionStates
?.values
?.sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
?.map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
?.forEach(::add)
}
val fieldConditionEffectIds =
currentResult.snapshot.field.conditionStates
.values
.sortedBy(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectOrder)
.map(io.github.lishangbu.avalon.game.battle.engine.core.model.AttachedEffectState::effectId)
val effectIds =
buildList {
unit.abilityId?.let(::add)
BattleHeldItemRuntimeSupport.activeItemId(unit, currentResult.snapshot.field)?.let(::add)
unit.statusState?.effectId?.let(::add)
addAll(unitConditionEffectIds)
addAll(unitVolatileEffectIds)
addAll(sideConditionEffectIds)
addAll(fieldConditionEffectIds)
currentResult.snapshot.field.weatherState
?.effectId
?.let(::add)
currentResult.snapshot.field.terrainState
?.effectId
?.let(::add)
}
effectIds.forEach { effectId ->
if (!effectRepository.contains(effectId)) {
return@forEach
}
currentResult =
processEffectHook(
snapshot = currentResult.snapshot,
hookName = hookName,
effect = effectRepository.get(effectId),
selfId = unitId,
targetId = targetId,
sourceId = sourceId,
relay = currentResult.relay,
attributes = attributes,
)
if (currentResult.cancelled) {
return currentResult
}
}
return currentResult
}
/**
* 处理某个 effect 自身的指定 hook。
*/
private fun processEffectHook(
snapshot: BattleRuntimeSnapshot,
hookName: String,
effect: EffectDefinition,
selfId: String?,
targetId: String?,
sourceId: String?,
relay: Any?,
attributes: Map<String, Any?>,
): HookPhaseResult {
val rules =
effect.hooks.entries
.firstOrNull { entry -> entry.key.value == hookName }
?.value
.orEmpty()
if (rules.isEmpty()) {
return HookPhaseResult(snapshot = snapshot, cancelled = false, relay = relay)
}
var currentSnapshot = snapshot
var currentRelay: Any? = relay
var cancelled = false
val sortedRules =
rules.sortedWith(
compareByDescending<io.github.lishangbu.avalon.game.battle.engine.core.dsl.HookRule> { rule -> rule.priority }
.thenByDescending { rule -> rule.subOrder },
)
for (rule in sortedRules) {
val currentSelfSide = selfId?.let { id -> sideOfUnit(currentSnapshot, id) }
val currentTargetSide = targetId?.let { id -> sideOfUnit(currentSnapshot, id) }
val context =
EventContext(
hookName =
io.github.lishangbu.avalon.game.battle.engine.core.type
.HookName(hookName),
battle = currentSnapshot.battle,
self = selfId?.let { id -> currentSnapshot.units[id] },
target = targetId?.let { id -> currentSnapshot.units[id] },
source = sourceId?.let { id -> currentSnapshot.units[id] },
side = currentSelfSide,
foeSide = currentTargetSide,
field = currentSnapshot.field,
effect = effect,
effectLookup = ::findEffectOrNull,
relay = currentRelay,
attributes = attributes,
)
val ruleResult = hookRuleProcessor.process(rule, context)
currentRelay = ruleResult.relay
currentSnapshot = applyMutations(currentSnapshot, selfId, targetId, sourceId, ruleResult.mutations)
if (ruleResult.cancelled) {
cancelled = true
break
}
}
return HookPhaseResult(
snapshot = currentSnapshot,
cancelled = cancelled,
relay = currentRelay,
)
}
/**
* 把一批 mutation 写回当前 battle 快照。
*/
private fun applyMutations(
snapshot: BattleRuntimeSnapshot,
selfId: String?,
targetId: String?,
sourceId: String?,
mutations: List<BattleMutation>,
): BattleRuntimeSnapshot {
if (mutations.isEmpty()) {
return snapshot
}
val filteredResult =
mutationInterceptorChain.filter(
snapshot = snapshot,
selfId = selfId,
targetId = targetId,
sourceId = sourceId,
mutations = mutations,
attachedEffectProcessor =
BattleAttachedEffectProcessor { attachedSnapshot, unitId, hookName, attachedTargetId, attachedSourceId, relay, attributes ->
processAttachedEffects(
snapshot = attachedSnapshot,
unitId = unitId,
hookName = hookName,
targetId = attachedTargetId,
sourceId = attachedSourceId,
relay = relay,
attributes = attributes,
)
},
)
val applyResult =
mutationApplier.apply(
mutations = filteredResult.mutations,
context =
MutationApplicationContext(
battle = filteredResult.snapshot.battle,
field = filteredResult.snapshot.field,
units = filteredResult.snapshot.units,
sides = filteredResult.snapshot.sides,
selfId = selfId,
targetId = targetId,
sourceId = sourceId,
side = selfId?.let { id -> sideOfUnit(filteredResult.snapshot, id) },
foeSide = targetId?.let { id -> sideOfUnit(filteredResult.snapshot, id) },
),
)
val appliedSnapshot =
filteredResult.snapshot.copy(
battle = applyResult.battle,
field = applyResult.field,
units = applyResult.units,
sides = applyResult.sides,
)
return triggeredHookDispatcher.dispatch(
snapshot = appliedSnapshot,
triggeredHooks = applyResult.triggeredHooks,
targetId = targetId,
sourceId = sourceId,
participantUnitIds = triggeredHookParticipants(appliedSnapshot, selfId, targetId, sourceId),
)
}
/**
* 提取本次结算里最直接相关的单位集合,供 `trigger_event` 这种局部后续 hook 使用。
*/
private fun triggeredHookParticipants(
snapshot: BattleRuntimeSnapshot,
selfId: String?,
targetId: String?,
sourceId: String?,
): List<String> = listOfNotNull(selfId, targetId, sourceId).distinct().filter { unitId -> unitId in snapshot.units }
/**
* 返回指定 phase 下挂载 effect 的处理顺序。
*/
private fun attachmentOrderForPhase(
hookName: String,
selfId: String,
targetId: String,
): List<String> =
when (hookName) {
StandardHookNames.ON_MODIFY_EVASION.value -> listOf(targetId)
StandardHookNames.ON_MODIFY_ACCURACY.value -> listOf(selfId)
StandardHookNames.ON_MODIFY_BASE_POWER.value -> listOf(selfId)
StandardHookNames.ON_MODIFY_ATTACK.value -> listOf(selfId)
StandardHookNames.ON_MODIFY_DEFENSE.value -> listOf(selfId)
StandardHookNames.ON_MODIFY_CRIT_RATIO.value -> listOf(selfId, targetId)
StandardHookNames.ON_MODIFY_STAB.value -> listOf(selfId)
StandardHookNames.ON_MODIFY_DAMAGE.value -> listOf(selfId, targetId)
StandardHookNames.ON_BEFORE_MOVE.value -> listOf(selfId)
StandardHookNames.ON_TRY_MOVE.value -> listOf(selfId)
StandardHookNames.ON_AFTER_MOVE.value -> listOf(selfId)
else -> listOf(selfId, targetId)
}
/**
* 从当前快照中读取指定单位状态。
*/
private fun requireUnit(
snapshot: BattleRuntimeSnapshot,
unitId: String,
) = requireNotNull(snapshot.units[unitId]) { "Unit '$unitId' was not found in snapshot." }
/**
* 根据单位标识反查其所属 side。
*/
private fun sideOfUnit(
snapshot: BattleRuntimeSnapshot,
unitId: String,
): SideState? = snapshot.sides.values.firstOrNull { side -> unitId in side.activeUnitIds || unitId in side.unitIds }
/**
* 以空值安全地查询一个 effect。
*/
private fun findEffectOrNull(effectId: String): EffectDefinition? = if (effectRepository.contains(effectId)) effectRepository.get(effectId) else null
}