BattleSessionLifecycleCoordinator.kt
package io.github.lishangbu.avalon.game.battle.engine.core.session
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
/**
* 管理回合末推进和手动替补提交流程。
*
* 它负责会话生命周期中两类“跨多步状态变更”的流程:
* - 回合结束后的 residual、替补和 turn 递增
* - 玩家对待处理替补请求提交具体 replacement choice
*/
internal class BattleSessionLifecycleCoordinator(
private val session: BattleSession,
private val sideConditionDurationManager: BattleSideConditionDurationManager = BattleSideConditionDurationManager(),
private val fieldEffectDurationManager: BattleFieldEffectDurationManager = BattleFieldEffectDurationManager(),
private val unitEffectDurationManager: BattleUnitEffectDurationManager = BattleUnitEffectDurationManager(),
) {
/**
* 提交一个手动替补选择。
*
* @param sideId 需要替补的 side
* @param incomingUnitId 被选中的替补单位
* @return 更新后的 battle snapshot
*/
fun submitReplacementChoice(
sideId: String,
incomingUnitId: String,
): BattleRuntimeSnapshot {
// 手动替补只允许发生在当前回合已经被打断、明确等待 replacement 的阶段。
session.ensureAwaitingReplacement()
val request =
requireNotNull(session.replacementRequests.firstOrNull { candidate -> candidate.sideId == sideId }) {
"No replacement request found for side '$sideId'."
}
require(incomingUnitId in request.candidateUnitIds) {
"Unit '$incomingUnitId' is not a valid replacement candidate for side '$sideId'."
}
val side = requireNotNull(session.currentSnapshot.sides[sideId]) { "Side '$sideId' was not found." }
val outgoingUnitId = request.outgoingUnitIds.firstOrNull()
if (outgoingUnitId != null) {
val outgoingUnit = session.currentSnapshot.units[outgoingUnitId]
if ((outgoingUnit?.currentHp ?: 0) > 0) {
session.currentSnapshot = session.processSwitchOut(outgoingUnitId, session.currentSnapshot)
}
session.currentSnapshot = BattleSessionSwitchBoostStateSupport.clearOutgoingBoosts(session.currentSnapshot, outgoingUnitId)
}
val nextActiveIds = listOf(incomingUnitId)
val nextSides = session.currentSnapshot.sides + (sideId to side.copy(activeUnitIds = nextActiveIds))
val replacedSnapshot = session.currentSnapshot.copy(sides = nextSides)
session.currentSnapshot =
session.clearForceSwitchRequests(
snapshot = replacedSnapshot,
unitIds =
buildList {
outgoingUnitId?.let(::add)
add(incomingUnitId)
},
)
session.currentSnapshot =
BattleSessionSwitchBoostStateSupport.applyIncomingBoostCarry(
snapshot = session.currentSnapshot,
outgoingUnitId = outgoingUnitId,
incomingUnitId = incomingUnitId,
)
session.currentSnapshot = session.processSwitchIn(incomingUnitId, session.currentSnapshot)
session.replacementRequests.removeIf { candidate -> candidate.sideId == sideId }
session.recordLog("Submitted replacement choice for side $sideId: $incomingUnitId.")
session.recordEvent(
BattleSessionAutoReplacedPayload(
sideId = sideId,
after = nextActiveIds,
manual = true,
),
)
return session.currentSnapshot
}
/**
* 推进到回合结束。
*
* 流程顺序固定为:
* 1. residual phase
* 2. 濒死/替补处理
* 3. turn + 1
*/
fun endTurn(): BattleRuntimeSnapshot {
session.ensureRunning()
val residualSnapshot = session.battleFlowEngine.resolveResidualPhase(session.currentSnapshot)
val advancedField = fieldEffectDurationManager.advance(residualSnapshot)
val advancedSide = sideConditionDurationManager.advance(advancedField.snapshot)
val advancedUnit = unitEffectDurationManager.advance(advancedSide.snapshot)
val advancedDurationSnapshot =
applyDurationExpirationMutations(
snapshot = advancedUnit.snapshot,
expirationMutations =
advancedField.expirationMutations +
advancedSide.expirationMutations +
advancedUnit.expirationMutations,
)
session.currentSnapshot = session.resolveFaintAndReplacement(advancedDurationSnapshot)
session.currentSnapshot =
session.currentSnapshot.copy(
battle = session.currentSnapshot.battle.copy(turn = session.currentSnapshot.battle.turn + 1),
)
session.recordLog("Turn ended. Advanced to turn ${session.currentSnapshot.battle.turn}.")
session.recordEvent(BattleSessionTurnEndedPayload)
return session.currentSnapshot
}
/**
* 把 duration 到期产生的移除 mutation 重新送回正式生命周期链。
*
* 这样“显式 remove”与“回合末自然到期 remove”都会走同一套 interceptor/applier。
*/
private fun applyDurationExpirationMutations(
snapshot: BattleRuntimeSnapshot,
expirationMutations: List<BattleSessionScopedMutation>,
): BattleRuntimeSnapshot {
if (expirationMutations.isEmpty()) {
return snapshot
}
var currentSnapshot = snapshot
expirationMutations.forEach { scopedMutation ->
currentSnapshot =
session.applyMutations(
snapshot = currentSnapshot,
selfId = scopedMutation.selfId,
targetId = scopedMutation.targetId,
sourceId = scopedMutation.sourceId,
mutations = listOf(scopedMutation.mutation),
)
}
return currentSnapshot
}
}