From: TheSaminator Date: Fri, 11 Feb 2022 15:07:28 +0000 (-0500) Subject: Implement admiral ranking-up and ship purchasing X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=587e29cbe2f8c7341321dbc520164e7fbf345c52;p=starship-fights Implement admiral ranking-up and ship purchasing --- diff --git a/src/commonMain/kotlin/starshipfights/game/admiralty.kt b/src/commonMain/kotlin/starshipfights/game/admiralty.kt index ab65771..ca2bd90 100644 --- a/src/commonMain/kotlin/starshipfights/game/admiralty.kt +++ b/src/commonMain/kotlin/starshipfights/game/admiralty.kt @@ -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) { diff --git a/src/commonMain/kotlin/starshipfights/game/ship_types.kt b/src/commonMain/kotlin/starshipfights/game/ship_types.kt index 7bba806..5e31a84 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_types.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_types.kt @@ -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, diff --git a/src/jsMain/kotlin/starshipfights/game/client_game.kt b/src/jsMain/kotlin/starshipfights/game/client_game.kt index 60691cb..4629b11 100644 --- a/src/jsMain/kotlin/starshipfights/game/client_game.kt +++ b/src/jsMain/kotlin/starshipfights/game/client_game.kt @@ -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() } } diff --git a/src/jsMain/kotlin/starshipfights/game/popup.kt b/src/jsMain/kotlin/starshipfights/game/popup.kt index a22aea1..e7e5802 100644 --- a/src/jsMain/kotlin/starshipfights/game/popup.kt +++ b/src/jsMain/kotlin/starshipfights/game/popup.kt @@ -413,7 +413,7 @@ sealed class Popup { } } - class GameOver(private val outcome: String) : Popup() { + class GameOver(private val outcome: String, private val finalState: GameState) : Popup() { override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) { p { style = "text-align:center" @@ -423,8 +423,10 @@ sealed class Popup { p { style = "text-align:center" - a(href = "/me") { - +"Return to Home Page" + val admiralId = finalState.admiralInfo(mySide).id + + a(href = "/admiral/${admiralId}") { + +"Exit Battle" } } } diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index 6dcc8a9..9729fc9 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -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(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()) } diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt index 1da19b7..100ad4f 100644 --- a/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt +++ b/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt @@ -240,7 +240,7 @@ object AdmiralNames { private val diadochiFemaleNames = listOf( "Lursha", "Jamoqena", - "Hikari", + "Lokoria", "Iekuna", "Shara", "Etugen", diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt index 56595d6..1aeb0ea 100644 --- a/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt +++ b/src/jvmMain/kotlin/starshipfights/data/admiralty/admirals.kt @@ -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 { + 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) = ShipInDrydock .associate { it.shipData.id to it.shipData } fun generateFleet(admiral: Admiral): List = 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] } } diff --git a/src/jvmMain/kotlin/starshipfights/data/data_documents.kt b/src/jvmMain/kotlin/starshipfights/data/data_documents.kt index 3cd0c44..f1631c4 100644 --- a/src/jvmMain/kotlin/starshipfights/data/data_documents.kt +++ b/src/jvmMain/kotlin/starshipfights/data/data_documents.kt @@ -40,6 +40,7 @@ interface DocumentTable> { suspend fun unique(vararg properties: KProperty1) suspend fun put(doc: T) + suspend fun set(id: Id, set: Bson): Boolean suspend fun get(id: Id): T? suspend fun del(id: Id) suspend fun all(): Flow @@ -88,6 +89,10 @@ private class DocumentTableImpl>(val kclass: KClass, priv collection().replaceOneById(doc.id, doc, ReplaceOptions().upsert(true)) } + override suspend fun set(id: Id, set: Bson): Boolean { + return collection().updateOneById(id, set).matchedCount != 0L + } + override suspend fun get(id: Id): T? { return collection().findOneById(id) } diff --git a/src/jvmMain/kotlin/starshipfights/data/data_routines.kt b/src/jvmMain/kotlin/starshipfights/data/data_routines.kt index 8960fa8..080a4d1 100644 --- a/src/jvmMain/kotlin/starshipfights/data/data_routines.kt +++ b/src/jvmMain/kotlin/starshipfights/data/data_routines.kt @@ -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) + } + } } } } diff --git a/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt b/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt index 0af46ef..c7b9add 100644 --- a/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt +++ b/src/jvmMain/kotlin/starshipfights/game/endpoints_game.kt @@ -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)) } } } diff --git a/src/jvmMain/kotlin/starshipfights/game/server_game.kt b/src/jvmMain/kotlin/starshipfights/game/server_game.kt index 4b08a72..e54ae90 100644 --- a/src/jvmMain/kotlin/starshipfights/game/server_game.kt +++ b/src/jvmMain/kotlin/starshipfights/game/server_game.kt @@ -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() }.toSet() - val escapedShips = shipWrecks.filterValues { it.isEscape }.keys.map { it.reinterpret() }.toSet() - val damagedShips = session.state.value.ships.filterValues { it.hullAmount < it.ship.durability.maxHullPoints }.keys.map { it.reinterpret() }.toSet() - val intactShips = session.state.value.ships.keys.map { it.reinterpret() }.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() }.toSet() + val escapedShips = shipWrecks.filterValues { it.isEscape }.keys.map { it.reinterpret() }.toSet() + val damagedShips = ships.filterValues { it.hullAmount < it.ship.durability.maxHullPoints }.keys.map { it.reinterpret() }.toSet() + val intactShips = ships.keys.map { it.reinterpret() }.toSet() - damagedShips + + val hostAdmiralId = gameState.hostInfo.id.reinterpret() + val guestAdmiralId = gameState.guestInfo.id.reinterpret() + + 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) + } + } +} diff --git a/src/jvmMain/kotlin/starshipfights/info/views_user.kt b/src/jvmMain/kotlin/starshipfights/info/views_user.kt index 0541d49..4a4d52b 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_user.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_user.kt @@ -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(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" + } + } + } } }