DefaultOwnedCreatureService.kt

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

import io.github.lishangbu.avalon.dataset.api.service.MoveReader
import io.github.lishangbu.avalon.dataset.api.service.StatReader
import io.github.lishangbu.avalon.game.entity.CreatureStorageBox
import io.github.lishangbu.avalon.game.entity.OwnedCreature
import io.github.lishangbu.avalon.game.entity.OwnedCreatureMove
import io.github.lishangbu.avalon.game.entity.OwnedCreatureStat
import io.github.lishangbu.avalon.game.repository.CreatureStorageBoxRepository
import io.github.lishangbu.avalon.game.repository.OwnedCreatureMoveRepository
import io.github.lishangbu.avalon.game.repository.OwnedCreatureRepository
import io.github.lishangbu.avalon.game.repository.OwnedCreatureStatRepository
import org.babyfish.jimmer.sql.ast.mutation.SaveMode
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.Instant

@Service
open class DefaultOwnedCreatureService(
    private val creatureStorageBoxRepository: CreatureStorageBoxRepository,
    private val ownedCreatureRepository: OwnedCreatureRepository,
    private val ownedCreatureStatRepository: OwnedCreatureStatRepository,
    private val ownedCreatureMoveRepository: OwnedCreatureMoveRepository,
    private val statReader: StatReader,
    private val moveReader: MoveReader,
) {
    @Transactional(rollbackFor = [Exception::class])
    open fun capture(context: PreparedCaptureContext): CapturedCreatureSummary {
        val now = Instant.now()
        val box = ensureDefaultBox(context.playerId, now)
        val slot = nextBoxSlot(box.id)
        val metadata = context.targetMetadata
        val ownedCreature =
            ownedCreatureRepository.save(
                OwnedCreature {
                    playerId = context.playerId
                    creatureId = metadata.creatureId
                    creatureSpeciesId = metadata.creatureSpeciesId
                    nickname = null
                    level = metadata.level
                    experience = metadata.requiredExperience
                    natureId = metadata.natureId
                    abilityInternalName = context.targetUnit.abilityId
                    currentHp = context.targetUnit.currentHp
                    maxHp = context.targetUnit.maxHp
                    statusState = context.targetUnit.statusState
                    storageType = STORAGE_TYPE_BOX
                    storageBoxId = box.id
                    storageSlot = slot
                    partySlot = null
                    capturedAt = now
                    captureItemId = context.ballItemId
                    captureSessionId = context.sessionId
                    sourceType = SOURCE_TYPE_CAPTURE
                    createdAt = now
                    updatedAt = now
                },
                SaveMode.INSERT_ONLY,
            )
        saveStats(ownedCreature.id, metadata)
        saveMoves(ownedCreature.id, context)

        return CapturedCreatureSummary(
            ownedCreatureId = ownedCreature.id.toString(),
            creatureId = metadata.creatureId.toString(),
            creatureSpeciesId = metadata.creatureSpeciesId.toString(),
            creatureInternalName = metadata.creatureInternalName,
            creatureName = metadata.creatureName,
        )
    }

    private fun ensureDefaultBox(
        playerId: Long,
        now: Instant,
    ): CreatureStorageBox =
        creatureStorageBoxRepository
            .findAll()
            .filter { box -> box.playerId == playerId }
            .sortedWith(compareBy<CreatureStorageBox> { box -> box.sortingOrder }.thenBy { box -> box.id })
            .firstOrNull()
            ?: creatureStorageBoxRepository.save(
                CreatureStorageBox {
                    this.playerId = playerId
                    name = DEFAULT_BOX_NAME
                    sortingOrder = 1
                    capacity = DEFAULT_BOX_CAPACITY
                    createdAt = now
                    updatedAt = now
                },
                SaveMode.INSERT_ONLY,
            )

    private fun nextBoxSlot(storageBoxId: Long): Int =
        (
            ownedCreatureRepository
                .findAll()
                .filter { creature -> creature.storageBoxId == storageBoxId }
                .mapNotNull { creature -> creature.storageSlot }
                .maxOrNull()
                ?: 0
        ) + 1

    private fun saveStats(
        ownedCreatureId: Long,
        metadata: BattleUnitMetadata,
    ) {
        val statIds =
            statReader.findStatIdsByInternalNames(
                metadata.calculatedStats.keys
                    .union(metadata.ivs.keys)
                    .union(metadata.evs.keys),
            )
        metadata.calculatedStats.keys.union(metadata.ivs.keys).union(metadata.evs.keys).forEach { statInternalName ->
            val statId =
                requireNotNull(statIds[statInternalName]) {
                    "Stat '$statInternalName' was not found."
                }
            ownedCreatureStatRepository.save(
                OwnedCreatureStat {
                    this.ownedCreatureId = ownedCreatureId
                    this.statId = statId
                    iv = metadata.ivs[statInternalName] ?: 31
                    ev = metadata.evs[statInternalName] ?: 0
                    calculatedValue = metadata.calculatedStats[statInternalName] ?: 0
                },
                SaveMode.INSERT_ONLY,
            )
        }
    }

    private fun saveMoves(
        ownedCreatureId: Long,
        context: PreparedCaptureContext,
    ) {
        val movesByInternalName = moveReader.findMovesByInternalNames(context.targetUnit.movePp.keys)
        context.targetUnit.movePp.entries.toList().forEachIndexed { index, (moveInternalName, currentPp) ->
            val moveData = requireNotNull(movesByInternalName[moveInternalName]) { "Move '$moveInternalName' was not found." }
            ownedCreatureMoveRepository.save(
                OwnedCreatureMove {
                    this.ownedCreatureId = ownedCreatureId
                    slot = index + 1
                    moveId = moveData.id
                    this.currentPp = currentPp
                    maxPp = moveData.pp ?: currentPp
                },
                SaveMode.INSERT_ONLY,
            )
        }
    }

    private companion object {
        const val DEFAULT_BOX_NAME: String = "Box 1"
        const val DEFAULT_BOX_CAPACITY: Int = 30
        const val STORAGE_TYPE_BOX: String = "box"
        const val SOURCE_TYPE_CAPTURE: String = "capture"
    }
}