Implement admiral ranking-up and ship purchasing
authorTheSaminator <TheSaminator@users.noreply.github.com>
Fri, 11 Feb 2022 15:07:28 +0000 (10:07 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Fri, 11 Feb 2022 15:07:28 +0000 (10:07 -0500)
12 files changed:
src/commonMain/kotlin/starshipfights/game/admiralty.kt
src/commonMain/kotlin/starshipfights/game/ship_types.kt
src/jsMain/kotlin/starshipfights/game/client_game.kt
src/jsMain/kotlin/starshipfights/game/popup.kt
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt
src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt
src/jvmMain/kotlin/starshipfights/data/data_documents.kt
src/jvmMain/kotlin/starshipfights/data/data_routines.kt
src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt
src/jvmMain/kotlin/starshipfights/game/server_game.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt

index ab65771f3e0676ec6e500ab21f1ce5fda4a7f1b2..ca2bd9073fbdb8588236233bb92610e5a264c53e 100644 (file)
@@ -3,15 +3,46 @@ package starshipfights.game
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
 
-enum class AdmiralRank(val maxShipWeightClass: ShipWeightClass) {
-       REAR_ADMIRAL(ShipWeightClass.CRUISER),
-       VICE_ADMIRAL(ShipWeightClass.CRUISER),
-       ADMIRAL(ShipWeightClass.BATTLECRUISER),
-       HIGH_ADMIRAL(ShipWeightClass.BATTLESHIP),
-       LORD_ADMIRAL(ShipWeightClass.COLOSSUS);
+enum class AdmiralRank {
+       REAR_ADMIRAL,
+       VICE_ADMIRAL,
+       ADMIRAL,
+       HIGH_ADMIRAL,
+       LORD_ADMIRAL;
+       
+       val maxShipWeightClass: ShipWeightClass
+               get() = when (this) {
+                       REAR_ADMIRAL -> ShipWeightClass.CRUISER
+                       VICE_ADMIRAL -> ShipWeightClass.CRUISER
+                       ADMIRAL -> ShipWeightClass.BATTLECRUISER
+                       HIGH_ADMIRAL -> ShipWeightClass.BATTLESHIP
+                       LORD_ADMIRAL -> ShipWeightClass.COLOSSUS
+               }
        
        val maxBattleSize: BattleSize
                get() = BattleSize.values().last { it.maxWeightClass <= maxShipWeightClass }
+       
+       val minAcumen: Int
+               get() = when (this) {
+                       REAR_ADMIRAL -> 0
+                       VICE_ADMIRAL -> 1000
+                       ADMIRAL -> 4000
+                       HIGH_ADMIRAL -> 9000
+                       LORD_ADMIRAL -> 16000
+               }
+       
+       val dailyWage: Int
+               get() = when (this) {
+                       REAR_ADMIRAL -> 40
+                       VICE_ADMIRAL -> 50
+                       ADMIRAL -> 60
+                       HIGH_ADMIRAL -> 70
+                       LORD_ADMIRAL -> 80
+               }
+       
+       companion object {
+               fun fromAcumen(acumen: Int) = values().firstOrNull { it.minAcumen <= acumen } ?: REAR_ADMIRAL
+       }
 }
 
 fun AdmiralRank.getDisplayName(faction: Faction) = when (faction) {
index 7bba806e5bc170380037082bc41f197b0ab5952d..5e31a840ec89933d451a5724b0c027dc223c33c5 100644 (file)
@@ -44,8 +44,17 @@ enum class ShipWeightClass(
                        LINE_SHIP -> 275
                        DREADNOUGHT -> 400
                }
+       
+       val isUnique: Boolean
+               get() = this == COLOSSUS
 }
 
+val ShipWeightClass.buyPrice: Int
+       get() = basePointCost + 25
+
+val ShipWeightClass.sellPrice: Int
+       get() = basePointCost - 25
+
 enum class ShipType(
        val faction: Faction,
        val weightClass: ShipWeightClass,
index 60691cb2fd13c361a45043dbd34e521633d7eace..4629b1126f9ff55693b89ccb83bdb1130bc18333 100644 (file)
@@ -121,7 +121,7 @@ private suspend fun GameNetworkInteraction.execute(token: String): String {
                        }.display()
                        
                        if (!opponentJoined)
-                               Popup.GameOver("Unfortunately, your opponent never entered the battle.").display()
+                               Popup.GameOver("Unfortunately, your opponent never entered the battle.", gameState.value).display()
                        
                        val sendActionsJob = launch {
                                while (true) {
@@ -201,6 +201,6 @@ suspend fun gameMain(side: GlobalSide, token: String, state: GameState) {
                val finalMessage = connectionJob.await()
                renderingJob.cancel()
                
-               Popup.GameOver(finalMessage).display()
+               Popup.GameOver(finalMessage, gameState.value).display()
        }
 }
index a22aea17fc042487afd1aed0ba7f0035c69eb94f..e7e58027e1427d9141d9a01be05b40e93fa3a1c5 100644 (file)
@@ -413,7 +413,7 @@ sealed class Popup<out T> {
                }
        }
        
-       class GameOver(private val outcome: String) : Popup<Nothing>() {
+       class GameOver(private val outcome: String, private val finalState: GameState) : Popup<Nothing>() {
                override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) {
                        p {
                                style = "text-align:center"
@@ -423,8 +423,10 @@ sealed class Popup<out T> {
                        p {
                                style = "text-align:center"
                                
-                               a(href = "/me") {
-                                       +"Return to Home Page"
+                               val admiralId = finalState.admiralInfo(mySide).id
+                               
+                               a(href = "/admiral/${admiralId}") {
+                                       +"Exit Battle"
                                }
                        }
                }
index 6dcc8a95cd6230710ecaad80dcc355e67b3df586..9729fc92479a84487a9f1c35de370d4f5139a4dd 100644 (file)
@@ -14,24 +14,22 @@ import io.ktor.routing.*
 import io.ktor.sessions.*
 import io.ktor.util.*
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.toList
 import kotlinx.coroutines.launch
 import kotlinx.html.*
 import kotlinx.serialization.json.JsonObject
 import kotlinx.serialization.json.JsonPrimitive
-import org.litote.kmongo.and
-import org.litote.kmongo.eq
-import org.litote.kmongo.ne
-import org.litote.kmongo.setValue
+import org.litote.kmongo.*
 import starshipfights.*
 import starshipfights.data.Id
-import starshipfights.data.admiralty.Admiral
-import starshipfights.data.admiralty.ShipInDrydock
-import starshipfights.data.admiralty.generateFleet
+import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
 import starshipfights.data.createNonce
-import starshipfights.game.AdmiralRank
 import starshipfights.game.Faction
+import starshipfights.game.ShipType
+import starshipfights.game.buyPrice
+import starshipfights.game.toUrlSlug
 import starshipfights.info.*
 import java.time.Instant
 import java.time.temporal.ChronoUnit
@@ -112,8 +110,8 @@ interface AuthProvider {
                                                name = form["name"]?.takeIf { it.isNotBlank() } ?: throw MissingRequestParameterException("name"),
                                                isFemale = form.getOrFail("sex") == "female",
                                                faction = Faction.valueOf(form.getOrFail("faction")),
-                                               // TODO change to Rear Admiral
-                                               rank = AdmiralRank.LORD_ADMIRAL
+                                               acumen = 0,
+                                               money = 500
                                        )
                                        val newShips = generateFleet(newAdmiral)
                                        
@@ -152,6 +150,51 @@ interface AuthProvider {
                                        redirect("/admiral/$admiralId")
                                }
                                
+                               get("/admiral/{id}/buy/{ship}") {
+                                       call.respondHtml(HttpStatusCode.OK, call.buyShipConfirmPage())
+                               }
+                               
+                               post("/admiral/{id}/buy/{ship}") {
+                                       val currentUser = call.getUserSession()?.user
+                                       val admiralId = call.parameters["id"]?.let { Id<Admiral>(it) }!!
+                                       val admiral = Admiral.get(admiralId)!!
+                                       
+                                       if (admiral.owningUser != currentUser) throw ForbiddenException()
+                                       
+                                       val shipType = call.parameters["ship"]?.let { param -> ShipType.values().singleOrNull { it.toUrlSlug() == param } }!!
+                                       
+                                       if (shipType.faction != admiral.faction || shipType.weightClass.rank > admiral.rank.maxShipWeightClass.rank)
+                                               throw NotFoundException()
+                                       
+                                       if (shipType.weightClass.buyPrice > admiral.money)
+                                               redirect("/admiral/${admiralId}/manage")
+                                       
+                                       val ownedShips = ShipInDrydock.select(ShipInDrydock::owningAdmiral eq admiralId).toList()
+                                       
+                                       if (shipType.weightClass.isUnique) {
+                                               val hasSameWeightClass = ownedShips.any { it.shipType.weightClass == shipType.weightClass }
+                                               if (hasSameWeightClass)
+                                                       redirect("/admiral/${admiralId}/manage")
+                                       }
+                                       
+                                       val shipNames = ownedShips.map { it.name }.toMutableSet()
+                                       val newShipName = newShipName(shipType.faction, shipType.weightClass, shipNames) ?: ShipNames.nameShip(shipType.faction, shipType.weightClass)
+                                       
+                                       val newShip = ShipInDrydock(
+                                               name = newShipName,
+                                               shipType = shipType,
+                                               status = DrydockStatus.Ready,
+                                               owningAdmiral = admiralId
+                                       )
+                                       
+                                       launch { ShipInDrydock.put(newShip) }
+                                       launch {
+                                               Admiral.set(admiralId, inc(Admiral::money, -shipType.weightClass.buyPrice))
+                                       }
+                                       
+                                       redirect("/admiral/${admiralId}/manage")
+                               }
+                               
                                get("/admiral/{id}/delete") {
                                        call.respondHtml(HttpStatusCode.OK, call.deleteAdmiralConfirmPage())
                                }
index 1da19b7b33836da146c364cb398d21b0bbb5695d..100ad4f0167bb48be352daf056e7a3575ca8a59f 100644 (file)
@@ -240,7 +240,7 @@ object AdmiralNames {
        private val diadochiFemaleNames = listOf(
                "Lursha",
                "Jamoqena",
-               "Hikari",
+               "Lokoria",
                "Iekuna",
                "Shara",
                "Etugen",
index 56595d62266f9e7f9bf6c4874c1f5ff9115cb424..1aeb0ea5e3eeee05406c749bcf7a2d56f7dcf748 100644 (file)
@@ -5,9 +5,16 @@ import kotlinx.coroutines.flow.toList
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import org.bson.conversions.Bson
+import org.litote.kmongo.and
 import org.litote.kmongo.eq
-import starshipfights.data.*
+import org.litote.kmongo.gte
+import org.litote.kmongo.lt
+import starshipfights.data.DataDocument
+import starshipfights.data.DocumentTable
+import starshipfights.data.Id
 import starshipfights.data.auth.User
+import starshipfights.data.invoke
 import starshipfights.game.*
 import java.time.Instant
 
@@ -22,8 +29,12 @@ data class Admiral(
        val isFemale: Boolean,
        
        val faction: Faction,
-       val rank: AdmiralRank,
+       val acumen: Int,
+       val money: Int,
 ) : DataDocument<Admiral> {
+       val rank: AdmiralRank
+               get() = AdmiralRank.fromAcumen(acumen)
+       
        val fullName: String
                get() = "${rank.getDisplayName(faction)} $name"
        
@@ -32,6 +43,15 @@ data class Admiral(
        })
 }
 
+infix fun AdmiralRank.Companion.eq(rank: AdmiralRank): Bson = when (rank.ordinal) {
+       0 -> Admiral::acumen lt AdmiralRank.values()[1].minAcumen
+       AdmiralRank.values().size - 1 -> Admiral::acumen gte rank.minAcumen
+       else -> and(
+               Admiral::acumen gte rank.minAcumen,
+               Admiral::acumen lt AdmiralRank.values()[rank.ordinal + 1].minAcumen
+       )
+}
+
 @Serializable
 sealed class DrydockStatus {
        @Serializable
@@ -92,15 +112,15 @@ suspend fun getAdmiralsShips(admiralId: Id<Admiral>) = ShipInDrydock
        .associate { it.shipData.id to it.shipData }
 
 fun generateFleet(admiral: Admiral): List<ShipInDrydock> = ShipWeightClass.values()
-       .flatMap {
+       .flatMap { swc ->
                val shipTypes = ShipType.values().filter { st ->
-                       st.weightClass == it && st.faction == admiral.faction
+                       st.weightClass == swc && st.faction == admiral.faction
                }.shuffled()
                
                if (shipTypes.isEmpty())
                        emptyList()
                else
-                       (0..((admiral.rank.maxShipWeightClass.rank - it.rank) * 2 + 1).coerceAtLeast(0)).map { i ->
+                       (0..((admiral.rank.maxShipWeightClass.rank - swc.rank) * 2 + 1).coerceAtLeast(0)).map { i ->
                                shipTypes[i % shipTypes.size]
                        }
        }
index 3cd0c44ad1c116b92e0723a6df8ef118e9f11643..f1631c448a2ab0440ad0baa0652ba14428166732 100644 (file)
@@ -40,6 +40,7 @@ interface DocumentTable<T : DataDocument<T>> {
        suspend fun unique(vararg properties: KProperty1<T, *>)
        
        suspend fun put(doc: T)
+       suspend fun set(id: Id<T>, set: Bson): Boolean
        suspend fun get(id: Id<T>): T?
        suspend fun del(id: Id<T>)
        suspend fun all(): Flow<T>
@@ -88,6 +89,10 @@ private class DocumentTableImpl<T : DataDocument<T>>(val kclass: KClass<T>, priv
                collection().replaceOneById(doc.id, doc, ReplaceOptions().upsert(true))
        }
        
+       override suspend fun set(id: Id<T>, set: Bson): Boolean {
+               return collection().updateOneById(id, set).matchedCount != 0L
+       }
+       
        override suspend fun get(id: Id<T>): T? {
                return collection().findOneById(id)
        }
index 8960fa8a5a7a18bfdf7e06a717803f96eec19375..080a4d15edb0a4efefc8e918ccd056decadb58cd 100644 (file)
@@ -2,16 +2,16 @@ package starshipfights.data
 
 import kotlinx.coroutines.*
 import org.litote.kmongo.div
+import org.litote.kmongo.inc
 import org.litote.kmongo.lt
 import org.litote.kmongo.setValue
-import starshipfights.data.admiralty.Admiral
-import starshipfights.data.admiralty.BattleRecord
-import starshipfights.data.admiralty.DrydockStatus
-import starshipfights.data.admiralty.ShipInDrydock
+import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
+import starshipfights.game.AdmiralRank
 import starshipfights.sfLogger
 import java.time.Instant
+import java.time.ZoneId
 import kotlin.coroutines.CoroutineContext
 
 object DataRoutines : CoroutineScope {
@@ -32,13 +32,34 @@ object DataRoutines : CoroutineScope {
                        // Repair ships
                        launch {
                                while (currentCoroutineContext().isActive) {
-                                       val now = Instant.now()
                                        launch {
+                                               val now = Instant.now()
                                                ShipInDrydock.update(ShipInDrydock::status / DrydockStatus.InRepair::until lt now, setValue(ShipInDrydock::status, DrydockStatus.Ready))
                                        }
                                        delay(300_000)
                                }
                        }
+                       
+                       // Pay admirals
+                       launch {
+                               var prevTime = Instant.now().atZone(ZoneId.systemDefault())
+                               while (currentCoroutineContext().isActive) {
+                                       val currTime = Instant.now().atZone(ZoneId.systemDefault())
+                                       if (currTime.dayOfWeek != prevTime.dayOfWeek)
+                                               launch {
+                                                       AdmiralRank.values().forEach { rank ->
+                                                               launch {
+                                                                       Admiral.update(
+                                                                               AdmiralRank eq rank,
+                                                                               inc(Admiral::money, rank.dailyWage)
+                                                                       )
+                                                               }
+                                                       }
+                                               }
+                                       prevTime = currTime
+                                       delay(900_000)
+                               }
+                       }
                }
        }
 }
index 0af46ef3e165996a223767f913fc517d32285eb3..c7b9addd63988a010e6aa82dc6112ff8575fba26 100644 (file)
@@ -6,6 +6,7 @@ import io.ktor.http.*
 import io.ktor.routing.*
 import io.ktor.websocket.*
 import kotlinx.coroutines.launch
+import org.litote.kmongo.setValue
 import starshipfights.auth.getUser
 import starshipfights.data.admiralty.getAllInGameAdmirals
 import starshipfights.data.auth.User
@@ -48,7 +49,7 @@ fun Routing.installGame() {
                matchmakingEndpoint(user)
                
                launch {
-                       User.put(user.copy(status = UserStatus.READY_FOR_BATTLE))
+                       User.set(user.id, setValue(User::status, UserStatus.READY_FOR_BATTLE))
                }
        }
        
@@ -70,7 +71,7 @@ fun Routing.installGame() {
                gameEndpoint(user, token)
                
                launch {
-                       User.put(user.copy(status = UserStatus.AVAILABLE))
+                       User.set(user.id, setValue(User::status, UserStatus.AVAILABLE))
                }
        }
 }
index 4b08a724b602267e723a2ada0fe08c00f5b6921a..e54ae90ac7afdd1640f6fe8ff97982cdcf0b5618 100644 (file)
@@ -9,9 +9,11 @@ import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
 import org.litote.kmongo.`in`
+import org.litote.kmongo.inc
 import org.litote.kmongo.setValue
 import starshipfights.data.DocumentTable
 import starshipfights.data.Id
+import starshipfights.data.admiralty.Admiral
 import starshipfights.data.admiralty.BattleRecord
 import starshipfights.data.admiralty.DrydockStatus
 import starshipfights.data.admiralty.ShipInDrydock
@@ -39,49 +41,7 @@ object GameManager {
                        val end = session.gameEnd.await()
                        val endedAt = Instant.now()
                        
-                       val destroyedShipStatus = DrydockStatus.InRepair(endedAt.plus(12, ChronoUnit.HOURS))
-                       val damagedShipStatus = DrydockStatus.InRepair(endedAt.plus(9, ChronoUnit.HOURS))
-                       val intactShipStatus = DrydockStatus.InRepair(endedAt.plus(6, ChronoUnit.HOURS))
-                       val escapedShipStatus = DrydockStatus.InRepair(endedAt.plus(3, ChronoUnit.HOURS))
-                       
-                       val shipWrecks = session.state.value.destroyedShips
-                       val destroyedShips = shipWrecks.filterValues { !it.isEscape }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
-                       val escapedShips = shipWrecks.filterValues { it.isEscape }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
-                       val damagedShips = session.state.value.ships.filterValues { it.hullAmount < it.ship.durability.maxHullPoints }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
-                       val intactShips = session.state.value.ships.keys.map { it.reinterpret<ShipInDrydock>() }.toSet() - damagedShips
-                       
-                       launch {
-                               ShipInDrydock.update(ShipInDrydock::id `in` destroyedShips, setValue(ShipInDrydock::status, destroyedShipStatus))
-                       }
-                       launch {
-                               ShipInDrydock.update(ShipInDrydock::id `in` damagedShips, setValue(ShipInDrydock::status, damagedShipStatus))
-                       }
-                       launch {
-                               ShipInDrydock.update(ShipInDrydock::id `in` intactShips, setValue(ShipInDrydock::status, intactShipStatus))
-                       }
-                       launch {
-                               ShipInDrydock.update(ShipInDrydock::id `in` escapedShips, setValue(ShipInDrydock::status, escapedShipStatus))
-                       }
-                       
-                       val battleRecord = BattleRecord(
-                               battleInfo = session.state.value.battleInfo,
-                               
-                               whenStarted = startedAt,
-                               whenEnded = endedAt,
-                               
-                               hostUser = hostInfo.user.id.reinterpret(),
-                               guestUser = guestInfo.user.id.reinterpret(),
-                               
-                               hostAdmiral = hostInfo.id.reinterpret(),
-                               guestAdmiral = guestInfo.id.reinterpret(),
-                               
-                               winner = end.winner,
-                               winMessage = end.message
-                       )
-                       
-                       launch {
-                               BattleRecord.put(battleRecord)
-                       }
+                       onGameEnd(session.state.value, end, startedAt, endedAt)
                }
                
                val hostId = createToken()
@@ -226,3 +186,66 @@ suspend fun DefaultWebSocketServerSession.gameEndpoint(user: User, token: String
        sendEventsJob.cancelAndJoin()
        receiveActionsJob.cancelAndJoin()
 }
+
+private suspend fun onGameEnd(gameState: GameState, gameEnd: GameEvent.GameEnd, startedAt: Instant, endedAt: Instant) {
+       val destroyedShipStatus = DrydockStatus.InRepair(endedAt.plus(12, ChronoUnit.HOURS))
+       val damagedShipStatus = DrydockStatus.InRepair(endedAt.plus(8, ChronoUnit.HOURS))
+       val intactShipStatus = DrydockStatus.InRepair(endedAt.plus(4, ChronoUnit.HOURS))
+       val escapedShipStatus = DrydockStatus.InRepair(endedAt.plus(4, ChronoUnit.HOURS))
+       
+       val shipWrecks = gameState.destroyedShips
+       val ships = gameState.ships
+       
+       val destroyedShips = shipWrecks.filterValues { !it.isEscape }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
+       val escapedShips = shipWrecks.filterValues { it.isEscape }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
+       val damagedShips = ships.filterValues { it.hullAmount < it.ship.durability.maxHullPoints }.keys.map { it.reinterpret<ShipInDrydock>() }.toSet()
+       val intactShips = ships.keys.map { it.reinterpret<ShipInDrydock>() }.toSet() - damagedShips
+       
+       val hostAdmiralId = gameState.hostInfo.id.reinterpret<Admiral>()
+       val guestAdmiralId = gameState.guestInfo.id.reinterpret<Admiral>()
+       
+       val hostAcumenGain = shipWrecks.values.filter { it.owner == GlobalSide.GUEST && !it.isEscape }.sumOf { it.ship.pointCost }
+       val guestAcumenGain = shipWrecks.values.filter { it.owner == GlobalSide.HOST && !it.isEscape }.sumOf { it.ship.pointCost }
+       
+       val battleRecord = BattleRecord(
+               battleInfo = gameState.battleInfo,
+               
+               whenStarted = startedAt,
+               whenEnded = endedAt,
+               
+               hostUser = gameState.hostInfo.user.id.reinterpret(),
+               guestUser = gameState.guestInfo.user.id.reinterpret(),
+               
+               hostAdmiral = hostAdmiralId,
+               guestAdmiral = guestAdmiralId,
+               
+               winner = gameEnd.winner,
+               winMessage = gameEnd.message
+       )
+       
+       coroutineScope {
+               launch {
+                       ShipInDrydock.update(ShipInDrydock::id `in` destroyedShips, setValue(ShipInDrydock::status, destroyedShipStatus))
+               }
+               launch {
+                       ShipInDrydock.update(ShipInDrydock::id `in` damagedShips, setValue(ShipInDrydock::status, damagedShipStatus))
+               }
+               launch {
+                       ShipInDrydock.update(ShipInDrydock::id `in` intactShips, setValue(ShipInDrydock::status, intactShipStatus))
+               }
+               launch {
+                       ShipInDrydock.update(ShipInDrydock::id `in` escapedShips, setValue(ShipInDrydock::status, escapedShipStatus))
+               }
+               
+               launch {
+                       Admiral.set(hostAdmiralId, inc(Admiral::acumen, hostAcumenGain))
+               }
+               launch {
+                       Admiral.set(guestAdmiralId, inc(Admiral::acumen, guestAcumenGain))
+               }
+               
+               launch {
+                       BattleRecord.put(battleRecord)
+               }
+       }
+}
index 0541d49c9089a7934282fc959f1152c5619c3e1b..4a4d52bb9b40115de17d4f24097a13fe8b8f7977 100644 (file)
@@ -1,6 +1,7 @@
 package starshipfights.info
 
 import io.ktor.application.*
+import io.ktor.features.*
 import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.firstOrNull
@@ -19,9 +20,7 @@ import starshipfights.data.admiralty.*
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
 import starshipfights.data.auth.UserStatus
-import starshipfights.game.Faction
-import starshipfights.game.GlobalSide
-import starshipfights.game.toUrlSlug
+import starshipfights.game.*
 import starshipfights.redirect
 import java.time.Instant
 
@@ -276,10 +275,9 @@ suspend fun ApplicationCall.createAdmiralPage(): HTML.() -> Unit {
                                }
                                textInput(name = "name") {
                                        id = "name"
+                                       
                                        autoComplete = false
                                        required = true
-                                       minLength = "2"
-                                       maxLength = "32"
                                }
                                p {
                                        label {
@@ -333,6 +331,7 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                
                Triple(admiral.await(), ships.await(), records.await())
        }
+       
        val recordRoles = records.mapNotNull {
                when (admiralId) {
                        it.hostAdmiral -> GlobalSide.HOST
@@ -369,7 +368,7 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                section {
                        h1 { +admiral.name }
                        p {
-                               +admiral.fullName
+                               b { +admiral.fullName }
                                +" is a flag officer of the "
                                +admiral.faction.navyName
                                +". "
@@ -460,8 +459,9 @@ suspend fun ApplicationCall.admiralPage(): HTML.() -> Unit {
                                                                }
                                                }
                                                td {
-                                                       +when (recordRoles[record.id]) {
-                                                               record.winner -> "Victory"
+                                                       +when (record.winner) {
+                                                               null -> "Stalemate"
+                                                               recordRoles[record.id] -> "Victory"
                                                                else -> "Defeat"
                                                        }
                                                }
@@ -479,6 +479,14 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
        
        if (admiral.owningUser != currentUser) throw ForbiddenException()
        
+       val ownedShips = ShipInDrydock.select(ShipInDrydock::owningAdmiral eq admiralId).toList()
+       
+       //val sellableShips = ownedShips.filter { it.status == DrydockStatus.Ready }.sortedBy { it.name }.sortedBy { it.shipType.weightClass.rank }
+       
+       val buyableShips = ShipType.values().filter { type ->
+               type.faction == admiral.faction && type.weightClass.rank <= admiral.rank.maxShipWeightClass.rank && type.weightClass.buyPrice <= admiral.money && (if (type.weightClass.isUnique) ownedShips.none { it.shipType.weightClass == type.weightClass } else true)
+       }.sortedBy { it.name }.sortedBy { it.weightClass.rank }
+       
        return page(
                "Managing ${admiral.name}", standardNavBar(), PageNavSidebar(
                        listOf(
@@ -496,10 +504,11 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                                        }
                                }
                                textInput(name = "name") {
+                                       id = "name"
+                                       autoComplete = false
+                                       
                                        required = true
                                        value = admiral.name
-                                       minLength = "4"
-                                       maxLength = "24"
                                }
                                p {
                                        label {
@@ -523,6 +532,20 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                                                +"Female"
                                        }
                                }
+                               h3 { +"Generate Random Name" }
+                               p {
+                                       AdmiralNameFlavor.values().forEachIndexed { i, flavor ->
+                                               if (i != 0)
+                                                       br
+                                               a(href = "#", classes = "generate-admiral-name") {
+                                                       attributes["data-flavor"] = flavor.toUrlSlug()
+                                                       +flavor.displayName
+                                               }
+                                       }
+                               }
+                               script {
+                                       unsafe { +"window.sfAdmiralNameGen = true;" }
+                               }
                                submitInput {
                                        value = "Submit Changes"
                                }
@@ -533,6 +556,85 @@ suspend fun ApplicationCall.manageAdmiralPage(): HTML.() -> Unit {
                                }
                        }
                }
+               section {
+                       h2 { +"Manage Fleet" }
+                       h3 { +"Buy New Ship" }
+                       table {
+                               tr {
+                                       th { +"Ship Class" }
+                                       th { +"Ship Cost" }
+                                       th { +Entities.nbsp }
+                               }
+                               buyableShips.forEach { st ->
+                                       tr {
+                                               td { +st.fullDisplayName }
+                                               td {
+                                                       +st.weightClass.buyPrice.toString()
+                                                       +" Electro-Ducats"
+                                               }
+                                               td {
+                                                       form(action = "/admiral/${admiralId}/buy/${st.toUrlSlug()}", method = FormMethod.get) {
+                                                               submitInput {
+                                                                       value = "Buy"
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+}
+
+suspend fun ApplicationCall.buyShipConfirmPage(): HTML.() -> Unit {
+       val currentUser = getUserSession()?.user
+       val admiralId = parameters["id"]?.let { Id<Admiral>(it) }!!
+       val admiral = Admiral.get(admiralId)!!
+       
+       if (admiral.owningUser != currentUser) throw ForbiddenException()
+       
+       val shipType = parameters["ship"]?.let { param -> ShipType.values().singleOrNull { it.toUrlSlug() == param } }!!
+       
+       if (shipType.faction != admiral.faction || shipType.weightClass.rank > admiral.rank.maxShipWeightClass.rank)
+               throw NotFoundException()
+       
+       if (shipType.weightClass.buyPrice > admiral.money) {
+               return page(
+                       "Too Expensive", null, null
+               ) {
+                       section {
+                               h1 { +"Too Expensive" }
+                               p {
+                                       +"Unfortunately, the ${shipType.fullDisplayName} is out of ${admiral.fullName}'s budget. It costs ${shipType.weightClass.buyPrice} Electro-Ducats, and ${admiral.name} only has ${admiral.money} Electro-Ducats."
+                               }
+                               form(method = FormMethod.get, action = "/admiral/${admiral.id}/manage") {
+                                       submitInput {
+                                               value = "Back"
+                                       }
+                               }
+                       }
+               }
+       }
+       
+       return page(
+               "Are You Sure?", null, null
+       ) {
+               section {
+                       h1 { +"Are You Sure?" }
+                       p {
+                               +"${admiral.fullName} is about to buy a ${shipType.fullDisplayName} for ${shipType.weightClass.buyPrice} Electro-Ducats."
+                       }
+                       form(method = FormMethod.get, action = "/admiral/${admiral.id}/manage") {
+                               submitInput {
+                                       value = "Cancel"
+                               }
+                       }
+                       form(method = FormMethod.post, action = "/admiral/${admiral.id}/buy/${shipType.toUrlSlug()}") {
+                               submitInput {
+                                       value = "Checkout"
+                               }
+                       }
+               }
        }
 }