DefaultCapturePreparationService.kt

package io.github.lishangbu.avalon.game.capture

import io.github.lishangbu.avalon.dataset.api.model.SpeciesInfo
import io.github.lishangbu.avalon.dataset.api.service.ItemReader
import io.github.lishangbu.avalon.dataset.api.service.SpeciesReader
import io.github.lishangbu.avalon.game.battle.engine.core.capture.CaptureContext
import io.github.lishangbu.avalon.game.battle.engine.core.model.UnitState
import io.github.lishangbu.avalon.game.battle.engine.core.runtime.flow.BattleRuntimeSnapshot
import io.github.lishangbu.avalon.game.repository.OwnedCreatureRepository
import io.github.lishangbu.avalon.game.repository.PlayerRepository
import org.springframework.stereotype.Service

@Service
open class DefaultCapturePreparationService(
    private val itemReader: ItemReader,
    private val speciesReader: SpeciesReader,
    private val playerRepository: PlayerRepository,
    private val ownedCreatureRepository: OwnedCreatureRepository,
) {
    /**
     * 解析捕捉扣库存所需的最小上下文。
     *
     * 这里不读取目标精灵或物种信息,只负责把外部字符串标识收敛成真实主键。
     */
    open fun resolveInventoryContext(
        playerId: String,
        ballItemId: String,
    ): CaptureInventoryContext {
        val resolvedPlayerId = requirePlayer(playerId)
        val item = loadItem(ballItemId)
        return CaptureInventoryContext(
            playerId = resolvedPlayerId,
            ballItemId = item.id,
            ballItemInternalName = item.internalName,
        )
    }

    open fun prepare(
        sessionId: String,
        snapshot: BattleRuntimeSnapshot,
        command: CaptureCommand,
        inventoryContext: CaptureInventoryContext = resolveInventoryContext(command.playerId, command.ballItemId),
    ): PreparedCaptureContext {
        val targetUnit =
            requireNotNull(snapshot.units[command.targetUnitId]) {
                "Target unit '${command.targetUnitId}' was not found."
            }
        val sourceUnit =
            command.sourceUnitId?.let { sourceUnitId ->
                requireNotNull(snapshot.units[sourceUnitId]) {
                    "Source unit '$sourceUnitId' was not found."
                }
            }
        val targetMetadata = readBattleUnitMetadata(targetUnit)
        val species = loadSpecies(targetMetadata.creatureSpeciesId)
        val captureRate = species.captureRate ?: targetMetadata.captureRate ?: 0
        val alreadyCaught =
            ownedCreatureRepository
                .findAll()
                .any { ownedCreature ->
                    ownedCreature.playerId == inventoryContext.playerId &&
                        ownedCreature.creatureSpeciesId == targetMetadata.creatureSpeciesId
                }

        return PreparedCaptureContext(
            sessionId = sessionId,
            playerId = inventoryContext.playerId,
            ballItemId = inventoryContext.ballItemId,
            ballItemInternalName = inventoryContext.ballItemInternalName,
            targetUnitId = command.targetUnitId,
            sourceUnitId = command.sourceUnitId,
            snapshot = snapshot,
            targetUnit = targetUnit,
            sourceUnit = sourceUnit,
            targetMetadata = targetMetadata.copy(captureRate = captureRate),
            battleContext =
                CaptureContext(
                    alreadyCaught = alreadyCaught,
                    isFishingEncounter = targetUnit.metadata.captureContext.isFishingEncounter,
                    isSurfEncounter = targetUnit.metadata.captureContext.isSurfEncounter,
                    isNight = targetUnit.metadata.captureContext.isNight,
                    isCave = targetUnit.metadata.captureContext.isCave,
                    isUltraBeast = targetUnit.metadata.captureContext.isUltraBeast,
                    targetLevel = targetMetadata.level,
                    targetWeight = targetUnit.metadata.captureContext.targetWeight ?: targetUnit.metadata.weight,
                    targetTypes = targetUnit.typeIds,
                ),
        )
    }

    private fun loadItem(internalName: String) =
        itemReader.findItemByInternalName(internalName)
            ?: error("Capture ball '$internalName' was not found.")

    private fun requirePlayer(playerId: String): Long {
        val resolvedPlayerId = playerId.toLongOrNull() ?: error("playerId must be a valid long value.")
        requireNotNull(playerRepository.findNullable(resolvedPlayerId)) {
            "Player '$resolvedPlayerId' was not found."
        }
        return resolvedPlayerId
    }

    private fun loadSpecies(speciesId: Long): SpeciesInfo =
        requireNotNull(speciesReader.findSpeciesById(speciesId)) {
            "Creature species '$speciesId' was not found."
        }

    private fun readBattleUnitMetadata(unit: UnitState): BattleUnitMetadata =
        BattleUnitMetadata(
            creatureId = requireNotNull(unit.metadata.creatureId) { "Unit '${unit.id}' is missing required metadata 'creatureId'." },
            creatureSpeciesId = requireNotNull(unit.metadata.creatureSpeciesId) { "Unit '${unit.id}' is missing required metadata 'creatureSpeciesId'." },
            creatureInternalName = requireNotNull(unit.metadata.creatureInternalName) { "Unit '${unit.id}' is missing required metadata 'creatureInternalName'." },
            creatureName = requireNotNull(unit.metadata.creatureName) { "Unit '${unit.id}' is missing required metadata 'creatureName'." },
            level = requireNotNull(unit.metadata.level) { "Unit '${unit.id}' is missing required metadata 'level'." },
            requiredExperience = unit.metadata.requiredExperience ?: 0,
            natureId = unit.metadata.natureId,
            captureRate = unit.metadata.captureRate,
            ivs = unit.metadata.ivs,
            evs = unit.metadata.evs,
            calculatedStats = unit.stats,
        )
}