DamageBattleMutationInterceptor.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.mutation.BattleMutation
import io.github.lishangbu.avalon.game.battle.engine.core.mutation.DamageMutation
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.MutationTargetSelectorResolver
/**
* 伤害 mutation 的生命周期拦截器。
*
* 设计意图:
* - 把 `DamageMutation` 真正接到 `on_damage` 生命周期上;
* - 让目标身上的 ability / item / status / side condition / weather / terrain
* 都能在伤害真正写回前拿到这次伤害事件;
* - 在 hook 返回数值 relay 时,把新的实际伤害量回写到 mutation。
*
* 当前约束:
* - 多目标 `DamageMutation` 仍然共用一个 mutation 负载;
* - 因此只有当所有目标都给出一致的新伤害数值时,才会把 relay rewrite 回写成固定值;
* - 如果不同目标想要不同伤害,后续应引入“按目标拆分 mutation”协议,而不是在这里隐式取最后一个 relay。
*/
class DamageBattleMutationInterceptor : BattleMutationInterceptor {
/**
* 排在状态类拦截器之后执行,保持“附加状态先判断,再处理数值类后果”的大致阅读顺序。
*/
override val order: Int = 200
override fun supports(mutation: BattleMutation): Boolean = mutation is DamageMutation
override fun intercept(
context: BattleMutationInterceptionContext,
attachedEffectProcessor: BattleAttachedEffectProcessor,
): BattleMutationInterceptionResult {
val mutation =
context.mutation as? DamageMutation
?: return BattleMutationInterceptionResult(context.snapshot, true, context.mutation)
val targetUnitIds =
MutationTargetSelectorResolver.resolve(
mutation.target,
BattleMutationInterceptorSupport.mutationApplicationContext(context),
)
var currentSnapshot = context.snapshot
var blocked = false
var rewrittenDamage: Int? = null
var rewriteConflicted = false
targetUnitIds.forEach { affectedTargetId ->
val affectedUnit = currentSnapshot.units[affectedTargetId] ?: return@forEach
val amount = BattleMutationInterceptorSupport.calculateAmount(affectedUnit, mutation.mode, mutation.value)
val result =
attachedEffectProcessor.process(
snapshot = currentSnapshot,
unitId = affectedTargetId,
hookName = StandardHookNames.ON_DAMAGE.value,
targetId = affectedTargetId,
sourceId = context.sourceId,
relay = amount,
attributes =
buildMap {
put("mode", mutation.mode)
put("value", mutation.value)
put("amount", amount)
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@forEach
}
val relayDamage = BattleMutationInterceptorSupport.normalizeIntegerRelay(result.relay) ?: return@forEach
if (targetUnitIds.size == 1) {
rewrittenDamage = relayDamage.coerceAtLeast(0)
return@forEach
}
if (rewrittenDamage == null) {
rewrittenDamage = relayDamage.coerceAtLeast(0)
} else if (rewrittenDamage != relayDamage.coerceAtLeast(0)) {
rewriteConflicted = true
}
}
val nextMutation =
if (!blocked && !rewriteConflicted && rewrittenDamage != null) {
mutation.copy(
mode = null,
value = requireNotNull(rewrittenDamage).toDouble(),
)
} else {
mutation
}
return BattleMutationInterceptionResult(
snapshot = currentSnapshot,
allowed = !blocked,
mutation = nextMutation,
)
}
}