BattleMutationInterceptorSupport.kt
package io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow
import io.github.lishangbu.avalon.game.battle.engine.core.constant.BattleTargetRelationValues
import io.github.lishangbu.avalon.game.battle.engine.core.model.SideState
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.apply.MutationApplicationContext
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleRelayReader
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.support.BattleStatStageSupport
/**
* battle mutation 拦截器的共用辅助方法集合。
*
* 设计意图:
* - 把各个拦截器都会重复做的目标解析上下文组装、关系判定、数值换算集中收口;
* - 保持“一个拦截器只关注一个生命周期 hook”的类职责,而不是每个文件都重复一套样板代码;
* - 让 `set_status / add_volatile / damage / heal / boost` 这些 mutation hook 的上下文语义保持一致。
*/
internal object BattleMutationInterceptorSupport {
/**
* 为 mutation target selector 解析组装一份完整上下文。
*
* 这里沿用 `DefaultBattleFlowPhaseProcessor` 的快照语义:
* - `selfId` 表示当前正在处理 hook 的 self;
* - `targetId` 表示当前 move / effect 主目标;
* - `sourceId` 表示这次 mutation 的来源单位。
*
* 拦截器本身不直接修改这些语义,只负责把它们原样透传给 selector 解析层。
*/
fun mutationApplicationContext(context: BattleMutationInterceptionContext): MutationApplicationContext =
MutationApplicationContext(
battle = context.snapshot.battle,
field = context.snapshot.field,
units = context.snapshot.units,
sides = context.snapshot.sides,
selfId = context.selfId,
targetId = context.targetId,
sourceId = context.sourceId,
side = sideOfUnit(context.snapshot, context.selfId),
foeSide = sideOfUnit(context.snapshot, context.targetId),
)
/**
* 计算当前 mutation 在单个目标身上的最终数值。
*
* 这里故意与 `DefaultMutationApplier` 中的换算规则保持一致,
* 避免拦截器看到的 relay 数值和最终落盘值出现两套语义。
*/
fun calculateAmount(
unit: UnitState,
mode: String?,
value: Double,
): Int =
when (mode) {
"max_hp_ratio" -> (unit.maxHp * value).toInt()
else -> value.toInt()
}
/**
* 把 hook relay 规范化为整数语义。
*
* 默认数值动作会返回 `Double`,而 `damage / heal / restore_pp` 最终仍按整数落盘,
* 因此这里统一把 relay 收口成 int。
*/
fun normalizeIntegerRelay(relay: Any?): Int? = BattleRelayReader.readInt(relay)
/**
* 把 hook relay 规范化为 boost 映射。
*
* 当前 `on_boost` 虽然还没有默认 DSL 级的 object relay 修改动作,
* 但运行时允许自定义执行器直接返回 map,这里提前定义可接受的结构:
* - key 必须是字符串 stat id;
* - value 必须是数值,并最终按整数 stage 处理。
*/
fun normalizeBoostRelay(relay: Any?): Map<String, Int>? {
val relayMap = relay as? Map<*, *> ?: return null
val normalized = mutableMapOf<String, Int>()
relayMap.forEach { (key, value) ->
val statId = key as? String ?: return null
val stageDelta = value as? Number ?: return null
normalized[statId] = stageDelta.toInt()
}
return BattleStatStageSupport.normalizeBoostPayload(normalized)
}
/**
* 把 hook relay 规范化为属性 id 列表。
*
* `on_change_type` 的新语义按“字符串数组”收口,不再只停留在 veto。
*/
fun normalizeTypeRelay(relay: Any?): List<String>? {
val relayValues =
when (relay) {
is Collection<*> -> relay.toList()
is Array<*> -> relay.toList()
else -> return null
}
return relayValues.map { value ->
value as? String ?: return null
}
}
/**
* 推导“source 对当前 target 的关系”。
*
* 该值会作为 attributes 透传给 hook,便于 effect 规则区分:
* - `self`
* - `ally`
* - `foe`
*
* 如果来源单位缺失,当前返回空值并由调用方决定是否写入 attributes。
*/
fun resolveTargetRelation(
snapshot: BattleRuntimeSnapshot,
sourceId: String?,
targetId: String,
): String? {
if (sourceId == null) {
return null
}
val sourceSideId = sideOfUnit(snapshot, sourceId)?.id ?: return null
val targetSideId = sideOfUnit(snapshot, targetId)?.id ?: return null
return when {
sourceId == targetId -> BattleTargetRelationValues.SELF
sourceSideId == targetSideId -> BattleTargetRelationValues.ALLY
else -> BattleTargetRelationValues.FOE
}
}
private fun sideOfUnit(
snapshot: BattleRuntimeSnapshot,
unitId: String?,
): SideState? =
snapshot.sides.values.firstOrNull { side ->
unitId in side.activeUnitIds || unitId in side.unitIds
}
}