ApplyConditionBattleMutationInterceptor.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.event.StandardHookNames
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplyConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplyFieldConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.ApplySideConditionMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.MutationTargetSelectorResolver
import io.github.lishangbu.avalon.game.battle.engine.core.type.StandardTargetSelectorIds
/**
* condition 挂载 mutation 的生命周期拦截器。
*
* 设计意图:
* - 把单位 condition、side condition 与 field condition 的“挂载前”生命周期统一收口到 `on_apply_condition`;
* - 让 ability / item / 状态 / 场地规则可以在 condition 真正写回前观察或拒绝本次挂载;
* - 保持三条路径共享同一 hook 名称,差异通过 attributes 暴露。
*
* 当前约定:
* - 单位 / side / field 都通过 `conditionScope` 区分作用域;
* - side condition 会把目标 side 的 active 单位作为 attached-effect 派发载体;
* - field condition 会把当前全部 active 单位作为 attached-effect 派发载体;
* - 若没有 active 单位,则 field / side 当前不会派发该 hook。
*/
class ApplyConditionBattleMutationInterceptor : BattleMutationInterceptor {
override val order: Int = 475
override fun supports(mutation: BattleMutation): Boolean = mutation is ApplyConditionMutation || mutation is ApplySideConditionMutation || mutation is ApplyFieldConditionMutation
override fun intercept(
context: BattleMutationInterceptionContext,
attachedEffectProcessor: BattleAttachedEffectProcessor,
): BattleMutationInterceptionResult =
when (val mutation = context.mutation) {
is ApplyConditionMutation -> interceptUnitCondition(context, mutation, attachedEffectProcessor)
is ApplySideConditionMutation -> interceptSideCondition(context, mutation, attachedEffectProcessor)
is ApplyFieldConditionMutation -> interceptFieldCondition(context, mutation, attachedEffectProcessor)
else -> BattleMutationInterceptionResult(context.snapshot, true, context.mutation)
}
private fun interceptUnitCondition(
context: BattleMutationInterceptionContext,
mutation: ApplyConditionMutation,
attachedEffectProcessor: BattleAttachedEffectProcessor,
): BattleMutationInterceptionResult {
val targetUnitIds =
MutationTargetSelectorResolver.resolve(
mutation.target,
BattleMutationInterceptorSupport.mutationApplicationContext(context),
)
var currentSnapshot = context.snapshot
var blocked = false
targetUnitIds.forEach { affectedTargetId ->
val result =
attachedEffectProcessor.process(
snapshot = currentSnapshot,
unitId = affectedTargetId,
hookName = StandardHookNames.ON_APPLY_CONDITION.value,
targetId = affectedTargetId,
sourceId = context.sourceId,
relay = true,
attributes =
buildMap {
put("conditionEffectId", mutation.conditionEffectId)
put("conditionScope", UNIT_SCOPE)
mutation.duration?.let { duration -> put("duration", duration) }
BattleMutationInterceptorSupport
.resolveTargetRelation(currentSnapshot, context.sourceId, affectedTargetId)
?.let { relation -> put(BattleAttributeKeys.TARGET_RELATION, relation) }
},
)
currentSnapshot = result.snapshot
if (result.cancelled || result.relay == false) {
blocked = true
}
}
return BattleMutationInterceptionResult(
snapshot = currentSnapshot,
allowed = !blocked,
mutation = mutation,
)
}
private fun interceptSideCondition(
context: BattleMutationInterceptionContext,
mutation: ApplySideConditionMutation,
attachedEffectProcessor: BattleAttachedEffectProcessor,
): BattleMutationInterceptionResult {
val targetSides = resolveTargetSides(context, mutation)
var currentSnapshot = context.snapshot
var blocked = false
targetSides.forEach { targetSide ->
targetSide.activeUnitIds.distinct().forEach { affectedTargetId ->
val result =
attachedEffectProcessor.process(
snapshot = currentSnapshot,
unitId = affectedTargetId,
hookName = StandardHookNames.ON_APPLY_CONDITION.value,
targetId = affectedTargetId,
sourceId = context.sourceId,
relay = true,
attributes =
buildMap {
put("conditionEffectId", mutation.conditionEffectId)
put("conditionScope", SIDE_SCOPE)
put("sideId", targetSide.id)
mutation.duration?.let { duration -> put("duration", duration) }
BattleMutationInterceptorSupport
.resolveTargetRelation(currentSnapshot, context.sourceId, affectedTargetId)
?.let { relation -> put(BattleAttributeKeys.TARGET_RELATION, relation) }
},
)
currentSnapshot = result.snapshot
if (result.cancelled || result.relay == false) {
blocked = true
}
}
}
return BattleMutationInterceptionResult(
snapshot = currentSnapshot,
allowed = !blocked,
mutation = mutation,
)
}
private fun interceptFieldCondition(
context: BattleMutationInterceptionContext,
mutation: ApplyFieldConditionMutation,
attachedEffectProcessor: BattleAttachedEffectProcessor,
): BattleMutationInterceptionResult {
val targetUnitIds =
context.snapshot.sides.values
.flatMap(SideState::activeUnitIds)
.distinct()
var currentSnapshot = context.snapshot
var blocked = false
targetUnitIds.forEach { affectedTargetId ->
val result =
attachedEffectProcessor.process(
snapshot = currentSnapshot,
unitId = affectedTargetId,
hookName = StandardHookNames.ON_APPLY_CONDITION.value,
targetId = affectedTargetId,
sourceId = context.sourceId,
relay = true,
attributes =
buildMap {
put("conditionEffectId", mutation.conditionEffectId)
put("conditionScope", FIELD_SCOPE)
mutation.duration?.let { duration -> put("duration", duration) }
BattleMutationInterceptorSupport
.resolveTargetRelation(currentSnapshot, context.sourceId, affectedTargetId)
?.let { relation -> put(BattleAttributeKeys.TARGET_RELATION, relation) }
},
)
currentSnapshot = result.snapshot
if (result.cancelled || result.relay == false) {
blocked = true
}
}
return BattleMutationInterceptionResult(
snapshot = currentSnapshot,
allowed = !blocked,
mutation = mutation,
)
}
private fun resolveTargetSides(
context: BattleMutationInterceptionContext,
mutation: ApplySideConditionMutation,
): List<SideState> {
val applicationContext = BattleMutationInterceptorSupport.mutationApplicationContext(context)
return when (mutation.target) {
StandardTargetSelectorIds.SIDE -> listOfNotNull(applicationContext.side)
StandardTargetSelectorIds.FOE_SIDE -> listOfNotNull(applicationContext.foeSide)
else -> emptyList()
}
}
private companion object {
private const val UNIT_SCOPE: String = "unit"
private const val SIDE_SCOPE: String = "side"
private const val FIELD_SCOPE: String = "field"
}
}