BattleSessionActionInvalidationResolver.kt
package io.github.lishangbu.avalon.game.battle.engine.core.session
/**
* 回合中途动作失效判定器。
*
* 设计意图:
* - 一些 action 在入队时是合法的,但在真正轮到执行前,战场已经被前置 action 改写。
* - 典型场景是“单位先被打倒,再轮到自己出手”,这时应当跳过该动作,而不是在更深层抛异常。
* - 把这层判定集中到 executor 前面,避免各个 handler 自己重复做脆弱判断。
*/
internal class BattleSessionActionInvalidationResolver(
private val session: BattleSession,
) {
/**
* 返回当前 action 的失效原因;为空表示仍可继续执行。
*/
fun resolve(action: BattleSessionAction): String? =
when (action) {
is BattleSessionSwitchAction -> resolveSwitchInvalidation(action)
is BattleSessionRunAction -> resolveRunInvalidation(action)
is BattleSessionTargetedAction -> resolveTargetedActionInvalidation(action)
is BattleSessionSubmittingAction -> resolveSubmittingActionInvalidation(action)
else -> null
}
/**
* 校验由具体单位提交的动作是否仍有执行资格。
*
* 这里优先看“单位是否还活着”,再看“单位是否仍在 active 槽位”,
* 这样返回给上层的原因更贴近实际战场变化。
*/
private fun resolveSubmittingActionInvalidation(action: BattleSessionSubmittingAction): String? {
val unit =
session.currentSnapshot.units[action.submittingUnitId]
?: return "Submitting unit '${action.submittingUnitId}' was not found."
if (unit.currentHp <= 0) {
return "Submitting unit '${action.submittingUnitId}' has fainted."
}
val activeSide =
session.currentSnapshot.sides.values.firstOrNull { side ->
action.submittingUnitId in side.activeUnitIds
}
if (activeSide == null) {
return "Submitting unit '${action.submittingUnitId}' is no longer active."
}
return null
}
/**
* 校验带显式目标的动作是否仍能命中原目标。
*
* 当前策略比较保守:
* - 目标不存在或已经倒下时直接跳过;
* - 捕捉动作还要求目标继续留在可捕捉 side 的 active 槽位里。
*/
private fun resolveTargetedActionInvalidation(action: BattleSessionTargetedAction): String? {
resolveSubmittingActionInvalidation(action)?.let { reason ->
return reason
}
val targetUnit =
session.currentSnapshot.units[action.targetUnitId]
?: return "Target unit '${action.targetUnitId}' was not found."
if (targetUnit.currentHp <= 0) {
return "Target unit '${action.targetUnitId}' has fainted."
}
if (action is BattleSessionCaptureAction) {
val capturableSideId = session.currentSnapshot.battle.capturableSideId
val capturableSide = capturableSideId?.let(session.currentSnapshot.sides::get)
if (capturableSide == null || action.targetUnitId !in capturableSide.activeUnitIds) {
return "Capture target '${action.targetUnitId}' is no longer capturable."
}
}
return null
}
/**
* 校验换人动作是否仍有意义。
*/
private fun resolveSwitchInvalidation(action: BattleSessionSwitchAction): String? {
val side =
session.currentSnapshot.sides[action.sideId]
?: return "Side '${action.sideId}' was not found."
if (action.outgoingUnitId !in side.activeUnitIds) {
return "Outgoing unit '${action.outgoingUnitId}' is no longer active on side '${action.sideId}'."
}
val incomingUnit =
session.currentSnapshot.units[action.incomingUnitId]
?: return "Incoming unit '${action.incomingUnitId}' was not found."
if (incomingUnit.currentHp <= 0) {
return "Incoming unit '${action.incomingUnitId}' has fainted."
}
return null
}
/**
* 校验逃跑动作是否仍有发起者。
*/
private fun resolveRunInvalidation(action: BattleSessionRunAction): String? {
val side =
session.currentSnapshot.sides[action.sideId]
?: return "Side '${action.sideId}' was not found."
val hasActiveUnit =
side.activeUnitIds.any { unitId ->
(session.currentSnapshot.units[unitId]?.currentHp ?: 0) > 0
}
return if (hasActiveUnit) {
null
} else {
"Side '${action.sideId}' has no active unit that can run."
}
}
}