From 0f6cd5085e10ddd8161a8376f31910993a24bfd9 Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Tue, 7 Jun 2022 19:18:14 -0400 Subject: [PATCH] Add subplots --- .../data/admiralty/admiral_names.kt | 12 + .../data/admiralty/ship_names.kt | 0 .../starshipfights/game/ai/ai_optimization.kt | 3 +- .../kotlin/starshipfights/game/game_packet.kt | 20 +- .../kotlin/starshipfights/game/game_state.kt | 34 +- .../starshipfights/game/game_subplots.kt | 296 ++++++++++++++++++ .../kotlin/starshipfights/game/matchmaking.kt | 12 + .../starshipfights/game/ship_modifiers.kt | 3 + .../kotlin/starshipfights/game/util.kt | 21 ++ .../kotlin/starshipfights/game/client_game.kt | 16 +- .../starshipfights/game/client_training.kt | 8 +- .../kotlin/starshipfights/game/game_ui.kt | 22 ++ .../kotlin/starshipfights/game/popup.kt | 8 +- src/jsMain/resources/style.css | 44 ++- .../starshipfights/game/game_start_jvm.kt | 3 +- .../kotlin/starshipfights/game/server_game.kt | 3 +- 16 files changed, 469 insertions(+), 36 deletions(-) rename src/{jvmMain => commonMain}/kotlin/starshipfights/data/admiralty/admiral_names.kt (97%) rename src/{jvmMain => commonMain}/kotlin/starshipfights/data/admiralty/ship_names.kt (100%) create mode 100644 src/commonMain/kotlin/starshipfights/game/game_subplots.kt create mode 100644 src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt b/src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt similarity index 97% rename from src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt rename to src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt index af93345..520829d 100644 --- a/src/jvmMain/kotlin/starshipfights/data/admiralty/admiral_names.kt +++ b/src/commonMain/kotlin/starshipfights/data/admiralty/admiral_names.kt @@ -1,5 +1,6 @@ package starshipfights.data.admiralty +import starshipfights.game.Faction import kotlin.random.Random enum class AdmiralNameFlavor { @@ -22,6 +23,17 @@ enum class AdmiralNameFlavor { AMERICAN -> "American" HISPANIC_AMERICAN -> "Hispanic-American" } + + companion object { + fun forFaction(faction: Faction) = when (faction) { + Faction.MECHYRDIA -> setOf(MECHYRDIA, TYLA, CALIBOR, OLYMPIA, DUTCH) + Faction.NDRC -> setOf(DUTCH) + Faction.MASRA_DRAETSEN -> setOf(CALIBOR, NORTHERN_DIADOCHI, SOUTHERN_DIADOCHI) + Faction.FELINAE_FELICES -> setOf(OLYMPIA) + Faction.ISARNAREYKK -> setOf(FULKREYKK) + Faction.VESTIGIUM -> setOf(AMERICAN, HISPANIC_AMERICAN) + } + } } object AdmiralNames { diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt b/src/commonMain/kotlin/starshipfights/data/admiralty/ship_names.kt similarity index 100% rename from src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt rename to src/commonMain/kotlin/starshipfights/data/admiralty/ship_names.kt diff --git a/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt b/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt index b2f0368..3b01eca 100644 --- a/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt +++ b/src/commonMain/kotlin/starshipfights/game/ai/ai_optimization.kt @@ -241,7 +241,8 @@ fun generateOptimizationInitialState(hostFaction: Faction, guestFaction: Faction faction = guestFaction, rank = rank ), - battleInfo = battleInfo + battleInfo = battleInfo, + subplots = emptySet(), ) } diff --git a/src/commonMain/kotlin/starshipfights/game/game_packet.kt b/src/commonMain/kotlin/starshipfights/game/game_packet.kt index 9334a2a..5231351 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_packet.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_packet.kt @@ -31,7 +31,12 @@ sealed class GameEvent { data class InvalidAction(val message: String) : GameEvent() @Serializable - data class GameEnd(val winner: GlobalSide?, val message: String) : GameEvent() + data class GameEnd( + val winner: GlobalSide?, + val message: String, + @Serializable(with = MapAsListSerializer::class) + val subplotOutcomes: Map = emptyMap() + ) : GameEvent() } fun GameState.after(player: GlobalSide, packet: PlayerAction): GameEvent = when (packet) { @@ -56,12 +61,21 @@ fun GameState.after(player: GlobalSide, packet: PlayerAction): GameEvent = when val loserName = admiralInfo(player).fullName val winnerName = admiralInfo(player.other).fullName - GameEvent.GameEnd(player.other, "$loserName never joined the battle, yielding victory to $winnerName!") + GameEvent.GameEnd(player.other, "$loserName never joined the battle, yielding victory to $winnerName!", emptyMap()) } PlayerAction.Disconnect -> { val loserName = admiralInfo(player).fullName val winnerName = admiralInfo(player.other).fullName - GameEvent.GameEnd(player.other, "$loserName has disconnected from the battle, yielding victory to $winnerName!") + GameEvent.GameEnd(player.other, "$loserName has disconnected from the battle, yielding victory to $winnerName!", emptyMap()) } +}.let { event -> + if (event is GameEvent.StateChange) { + val subplotKeys = event.newState.subplots.map { it.key } + val finalState = subplotKeys.fold(event.newState) { newState, key -> + val subplot = newState.subplots.single { it.key == key } + subplot.onGameStateChanged(newState) + } + GameEvent.StateChange(finalState) + } else event } diff --git a/src/commonMain/kotlin/starshipfights/game/game_state.kt b/src/commonMain/kotlin/starshipfights/game/game_state.kt index 7558530..231362b 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_state.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_state.kt @@ -11,6 +11,8 @@ data class GameState( val guestInfo: InGameAdmiral, val battleInfo: BattleInfo, + val subplots: Set, + val phase: GamePhase = GamePhase.Deploy, val doneWithPhase: GlobalSide? = null, val calculatedInitiative: GlobalSide? = null, @@ -21,7 +23,10 @@ data class GameState( val chatBox: List = emptyList(), ) { fun getShipInfo(id: Id) = destroyedShips[id]?.ship ?: ships.getValue(id).ship + fun getShipInfoOrNull(id: Id) = destroyedShips[id]?.ship ?: ships[id]?.ship + fun getShipOwner(id: Id) = destroyedShips[id]?.owner ?: ships.getValue(id).owner + fun getShipOwnerOrNull(id: Id) = destroyedShips[id]?.owner ?: ships[id]?.owner } val GameState.currentInitiative: GlobalSide? @@ -47,6 +52,17 @@ private fun GameState.afterPhase(): GameState { var newInitiative: GameState.() -> InitiativePair = { InitiativePair(emptyMap()) } when (phase) { + GamePhase.Deploy -> { + return subplots.map { it.key }.fold(this) { newState, key -> + val subplot = newState.subplots.single { it.key == key } + subplot.onAfterDeployShips(newState) + }.copy( + phase = phase.next(), + ships = ships.mapValues { (_, ship) -> + ship.copy(isDoneCurrentPhase = false) + }, + ) + } is GamePhase.Power -> { // Prepare for move phase newInitiative = { calculateMovePhaseInitiative() } @@ -162,12 +178,24 @@ fun GameState.checkVictory(): GameEvent.GameEnd? { val hostDefeated = ships.none { (_, it) -> it.owner == GlobalSide.HOST } val guestDefeated = ships.none { (_, it) -> it.owner == GlobalSide.GUEST } + val winner = if (hostDefeated && guestDefeated) + null + else if (hostDefeated) + GlobalSide.GUEST + else if (guestDefeated) + GlobalSide.HOST + else return null + + val subplotsOutcomes = subplots.associate { subplot -> + subplot.key to subplot.getFinalGameResult(this, winner) + } + return if (hostDefeated && guestDefeated) - GameEvent.GameEnd(null, "Stalemate: both sides have been completely destroyed!") + GameEvent.GameEnd(null, "Stalemate: both sides have been completely destroyed!", subplotsOutcomes) else if (hostDefeated) - GameEvent.GameEnd(GlobalSide.GUEST, victoryMessage(GlobalSide.GUEST)) + GameEvent.GameEnd(GlobalSide.GUEST, victoryMessage(GlobalSide.GUEST), subplotsOutcomes) else if (guestDefeated) - GameEvent.GameEnd(GlobalSide.HOST, victoryMessage(GlobalSide.HOST)) + GameEvent.GameEnd(GlobalSide.HOST, victoryMessage(GlobalSide.HOST), subplotsOutcomes) else null } diff --git a/src/commonMain/kotlin/starshipfights/game/game_subplots.kt b/src/commonMain/kotlin/starshipfights/game/game_subplots.kt new file mode 100644 index 0000000..8b41938 --- /dev/null +++ b/src/commonMain/kotlin/starshipfights/game/game_subplots.kt @@ -0,0 +1,296 @@ +package starshipfights.game + +import kotlinx.serialization.Serializable +import starshipfights.data.Id + +@Serializable +data class GameObjective( + val displayText: String, + val succeeded: Boolean? +) + +fun GameState.objectives(forPlayer: GlobalSide): List = listOf( + GameObjective("Destroy or rout the enemy fleet", null) +) + subplots.filter { it.forPlayer == forPlayer }.mapNotNull { it.displayObjective(this) } + +@Serializable +data class SubplotKey( + val type: SubplotType, + val player: GlobalSide, +) + +val Subplot.key: SubplotKey + get() = SubplotKey(type, forPlayer) + +@Serializable +sealed class Subplot { + abstract val type: SubplotType + abstract val displayName: String + abstract val forPlayer: GlobalSide + + override fun equals(other: Any?): Boolean { + return other is Subplot && other.key == key + } + + override fun hashCode(): Int { + return key.hashCode() + } + + abstract fun displayObjective(gameState: GameState): GameObjective? + + abstract fun onAfterDeployShips(gameState: GameState): GameState + abstract fun onGameStateChanged(gameState: GameState): GameState + abstract fun getFinalGameResult(gameState: GameState, winner: GlobalSide?): SubplotOutcome + + protected fun GameState.modifySubplotData(newSubplot: Subplot) = copy(subplots = (subplots - this@Subplot) + newSubplot) + + @Serializable + class ExtendedDuty(override val forPlayer: GlobalSide) : Subplot() { + override val type: SubplotType + get() = SubplotType.EXTENDED_DUTY + + override val displayName: String + get() = "Extended Duty" + + override fun displayObjective(gameState: GameState) = GameObjective("Win the battle with your fleet worn out from extended duty", null) + + private fun ShipInstance.preBattleDamage(): ShipInstance = when ((0..4).random()) { + 0 -> copy( + hullAmount = (2..hullAmount).random(), + troopsAmount = (2..troopsAmount).random(), + modulesStatus = ShipModulesStatus( + modulesStatus.statuses.mapValues { (_, status) -> + if (status != ShipModuleStatus.ABSENT && (1..3).random() == 1) + ShipModuleStatus.DESTROYED + else + status + } + ) + ) + 1 -> copy( + hullAmount = (2..hullAmount).random(), + troopsAmount = (2..troopsAmount).random(), + ) + 2 -> copy( + troopsAmount = (2..troopsAmount).random(), + ) + else -> this + } + + override fun onAfterDeployShips(gameState: GameState) = gameState.copy(ships = gameState.ships.mapValues { (_, ship) -> + if (ship.owner == forPlayer) + ship.preBattleDamage() + else ship + }) + + override fun onGameStateChanged(gameState: GameState) = gameState + + override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = SubplotOutcome.fromBattleWinner(winner, forPlayer) + } + + @Serializable + class NoQuarter(override val forPlayer: GlobalSide) : Subplot() { + override val type: SubplotType + get() = SubplotType.NO_QUARTER + + override val displayName: String + get() = "Leave No Quarter!" + + override fun displayObjective(gameState: GameState): GameObjective { + val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other } + val enemyWrecks = gameState.destroyedShips.values.filter { it.owner == forPlayer.other } + + val totalEnemyShipPointCount = enemyShips.sumOf { it.ship.pointCost } + enemyWrecks.sumOf { it.ship.pointCost } + val escapedShipPointCount = enemyWrecks.filter { it.isEscape }.sumOf { it.ship.pointCost } + val destroyedShipPointCount = enemyWrecks.filter { !it.isEscape }.sumOf { it.ship.pointCost } + + val success = when { + destroyedShipPointCount * 2 >= totalEnemyShipPointCount -> true + escapedShipPointCount * 2 >= totalEnemyShipPointCount -> false + else -> null + } + + return GameObjective("Destroy at least half of the enemy fleet's point value - do not let them escape!", success) + } + + override fun onAfterDeployShips(gameState: GameState) = gameState + + override fun onGameStateChanged(gameState: GameState) = gameState + + override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?): SubplotOutcome { + val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other } + val enemyWrecks = gameState.destroyedShips.values.filter { it.owner == forPlayer.other } + + val totalEnemyShipPointCount = enemyShips.sumOf { it.ship.pointCost } + enemyWrecks.sumOf { it.ship.pointCost } + val destroyedShipPointCount = enemyWrecks.filter { !it.isEscape }.sumOf { it.ship.pointCost } + + return if (destroyedShipPointCount * 2 >= totalEnemyShipPointCount) + SubplotOutcome.WON + else + SubplotOutcome.LOST + } + } + + @Serializable + class Vendetta private constructor(override val forPlayer: GlobalSide, private val againstShip: Id?, private val outcome: SubplotOutcome) : Subplot() { + constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED) + constructor(forPlayer: GlobalSide, againstShip: Id) : this(forPlayer, againstShip, SubplotOutcome.UNDECIDED) + + override val type: SubplotType + get() = SubplotType.VENDETTA + + override val displayName: String + get() = "Vendetta!" + + override fun displayObjective(gameState: GameState): GameObjective? { + val shipName = gameState.getShipInfoOrNull(againstShip ?: return null)?.fullName ?: return null + return GameObjective("Destroy the $shipName", outcome.toSuccess) + } + + override fun onAfterDeployShips(gameState: GameState): GameState { + if (gameState.ships[againstShip] != null) return gameState + + val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other } + val highestEnemyShipTier = enemyShips.maxOf { it.ship.shipType.weightClass } + val enemyShipsOfHighestTier = enemyShips.filter { it.ship.shipType.weightClass == highestEnemyShipTier } + + val vendettaShip = enemyShipsOfHighestTier.random().id + return gameState.modifySubplotData(Vendetta(forPlayer, vendettaShip, SubplotOutcome.UNDECIDED)) + } + + override fun onGameStateChanged(gameState: GameState): GameState { + if (outcome != SubplotOutcome.UNDECIDED) return gameState + + val vendettaShipWreck = gameState.destroyedShips[againstShip ?: return gameState] ?: return gameState + return if (vendettaShipWreck.isEscape) + gameState.modifySubplotData(Vendetta(forPlayer, againstShip, SubplotOutcome.LOST)) + else + gameState.modifySubplotData(Vendetta(forPlayer, againstShip, SubplotOutcome.WON)) + } + + override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = if (outcome == SubplotOutcome.UNDECIDED) + SubplotOutcome.LOST + else outcome + } + + @Serializable + class RecoverInformant private constructor(override val forPlayer: GlobalSide, private val onBoardShip: Id?, private val outcome: SubplotOutcome, private val mostRecentChatMessages: Moment?) : Subplot() { + constructor(forPlayer: GlobalSide) : this(forPlayer, null, SubplotOutcome.UNDECIDED, null) + constructor(forPlayer: GlobalSide, onBoardShip: Id) : this(forPlayer, onBoardShip, SubplotOutcome.UNDECIDED, null) + + override val type: SubplotType + get() = SubplotType.RECOVER_INFORMANT + + override val displayName: String + get() = "Recover Informant" + + override fun displayObjective(gameState: GameState): GameObjective? { + val shipName = gameState.getShipInfoOrNull(onBoardShip ?: return null)?.fullName ?: return null + return GameObjective("Board the $shipName and recover your informant", outcome.toSuccess) + } + + override fun onAfterDeployShips(gameState: GameState): GameState { + if (gameState.ships[onBoardShip] != null) return gameState + + val enemyShips = gameState.ships.values.filter { it.owner == forPlayer.other } + val lowestEnemyShipTier = enemyShips.minOf { it.ship.shipType.weightClass } + val enemyShipsNotOfLowestTier = enemyShips.filter { it.ship.shipType.weightClass != lowestEnemyShipTier }.ifEmpty { enemyShips } + + val informantShip = enemyShipsNotOfLowestTier.random().id + return gameState.modifySubplotData(RecoverInformant(forPlayer, informantShip, SubplotOutcome.UNDECIDED, null)) + } + + private fun GameState.getNewMessages(readTime: Moment?) = if (readTime == null) + chatBox + else + chatBox.filter { it.sentAt > readTime } + + override fun onGameStateChanged(gameState: GameState): GameState { + if (outcome != SubplotOutcome.UNDECIDED) return gameState + + var readTime = mostRecentChatMessages + for (message in gameState.getNewMessages(mostRecentChatMessages)) { + when (message) { + is ChatEntry.ShipEscaped -> if (message.ship == onBoardShip) + return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.LOST, null)) + is ChatEntry.ShipDestroyed -> if (message.ship == onBoardShip) + return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.LOST, null)) + is ChatEntry.ShipBoarded -> if (message.ship == onBoardShip && (1..3).random() == 1) + return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.WON, null)) + else -> { + // do nothing + } + } + readTime = if (readTime == null || readTime < message.sentAt) message.sentAt else readTime + } + + return gameState.modifySubplotData(RecoverInformant(forPlayer, onBoardShip, SubplotOutcome.UNDECIDED, readTime)) + } + + override fun getFinalGameResult(gameState: GameState, winner: GlobalSide?) = if (outcome == SubplotOutcome.UNDECIDED) + SubplotOutcome.LOST + else outcome + } +} + +enum class SubplotType(val factory: (GlobalSide) -> Subplot) { + EXTENDED_DUTY(Subplot::ExtendedDuty), + NO_QUARTER(Subplot::NoQuarter), + VENDETTA(Subplot::Vendetta), + RECOVER_INFORMANT(Subplot::RecoverInformant), +} + +fun generateSubplots(battleSize: BattleSize, forPlayer: GlobalSide): Set = + (1..battleSize.numSubplotsPerPlayer).map { + SubplotType.values().random().factory(forPlayer) + }.toSet() + +@Serializable +enum class SubplotOutcome { + UNDECIDED, WON, LOST; + + val toSuccess: Boolean? + get() = when (this) { + UNDECIDED -> null + WON -> true + LOST -> false + } + + companion object { + fun fromBattleWinner(winner: GlobalSide?, subplotForPlayer: GlobalSide) = when (winner) { + subplotForPlayer -> WON + subplotForPlayer.other -> LOST + else -> UNDECIDED + } + } +} + +fun victoryTitle(player: GlobalSide, winner: GlobalSide?, subplotOutcomes: Map): String { + val myOutcomes = subplotOutcomes.filterKeys { it.player == player } + + return when (winner) { + player -> { + val isGlorious = myOutcomes.all { (_, outcome) -> outcome == SubplotOutcome.WON } + val isPyrrhic = myOutcomes.size >= 2 && myOutcomes.any { (_, outcome) -> outcome != SubplotOutcome.WON } + + if (isGlorious) + "Glorious Victory" + else if (isPyrrhic) + "Pyrrhic Victory" + else + "Victory" + } + player.other -> { + val isHeroic = myOutcomes.all { (_, outcome) -> outcome == SubplotOutcome.WON } + val isHumiliating = myOutcomes.size >= 2 && myOutcomes.any { (_, outcome) -> outcome != SubplotOutcome.WON } + + if (isHeroic) + "Heroic Defeat" + else if (isHumiliating) + "Humiliating Defeat" + else + "Defeat" + } + else -> "Stalemate" + } +} diff --git a/src/commonMain/kotlin/starshipfights/game/matchmaking.kt b/src/commonMain/kotlin/starshipfights/game/matchmaking.kt index 88ea5b8..bbfe00a 100644 --- a/src/commonMain/kotlin/starshipfights/game/matchmaking.kt +++ b/src/commonMain/kotlin/starshipfights/game/matchmaking.kt @@ -14,6 +14,18 @@ enum class BattleSize(val numPoints: Int, val maxWeightClass: ShipWeightClass, v CRUCIBLE_OF_HISTORY(3000, ShipWeightClass.COLOSSUS, "Crucible of History"); } +val BattleSize.numSubplotsPerPlayer: Int + get() = when (this) { + BattleSize.SKIRMISH -> 0 + BattleSize.RAID -> 0 + BattleSize.FIREFIGHT -> 0 + BattleSize.BATTLE -> (0..1).random() + BattleSize.GRAND_CLASH -> 1 + BattleSize.APOCALYPSE -> 1 + BattleSize.LEGENDARY_STRUGGLE -> 1 + BattleSize.CRUCIBLE_OF_HISTORY -> (1..2).random() + } + enum class BattleBackground(val displayName: String, val color: String) { BLUE_BROWN("Milky Way", "#335577"), BLUE_MAGENTA("Arcane Anomaly", "#553377"), diff --git a/src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt b/src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt new file mode 100644 index 0000000..6d6cbd1 --- /dev/null +++ b/src/commonMain/kotlin/starshipfights/game/ship_modifiers.kt @@ -0,0 +1,3 @@ +package starshipfights.game + + diff --git a/src/commonMain/kotlin/starshipfights/game/util.kt b/src/commonMain/kotlin/starshipfights/game/util.kt index 07807e9..6618d98 100644 --- a/src/commonMain/kotlin/starshipfights/game/util.kt +++ b/src/commonMain/kotlin/starshipfights/game/util.kt @@ -1,6 +1,12 @@ package starshipfights.game import kotlinx.html.* +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.PairSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import kotlin.math.abs import kotlin.math.exp @@ -15,6 +21,21 @@ val jsonSerializer = Json { useAlternativeNames = false } +class MapAsListSerializer(keySerializer: KSerializer, valueSerializer: KSerializer) : KSerializer> { + private val inner = ListSerializer(PairSerializer(keySerializer, valueSerializer)) + + override val descriptor: SerialDescriptor + get() = inner.descriptor + + override fun serialize(encoder: Encoder, value: Map) { + inner.serialize(encoder, value.toList()) + } + + override fun deserialize(decoder: Decoder): Map { + return inner.deserialize(decoder).toMap() + } +} + const val EPSILON = 0.00_001 fun > T.toUrlSlug() = name.replace('_', '-').lowercase() diff --git a/src/jsMain/kotlin/starshipfights/game/client_game.kt b/src/jsMain/kotlin/starshipfights/game/client_game.kt index 6448316..2a43721 100644 --- a/src/jsMain/kotlin/starshipfights/game/client_game.kt +++ b/src/jsMain/kotlin/starshipfights/game/client_game.kt @@ -114,8 +114,8 @@ suspend fun GameRenderInteraction.execute(scope: CoroutineScope) { } } -private suspend fun GameNetworkInteraction.execute(token: String): Pair { - val gameEnd = CompletableDeferred>() +private suspend fun GameNetworkInteraction.execute(token: String): GameEvent.GameEnd { + val gameEnd = CompletableDeferred() try { httpClient.webSocket("$rootPathWs/game/$token") { @@ -124,7 +124,7 @@ private suspend fun GameNetworkInteraction.execute(token: String): Pair { - gameEnd.complete(event.winner?.relativeTo(mySide) to event.message) + gameEnd.complete(event) closeAndReturn { return@webSocket sendActionsJob.cancel() } } } } } } catch (ex: WebSocketException) { - gameEnd.complete(null to "Server closed connection abruptly") + gameEnd.complete(GameEvent.GameEnd(null, "Server closed connection abruptly", emptyMap())) } if (gameEnd.isActive) - gameEnd.complete(null to "Connection closed") + gameEnd.complete(GameEvent.GameEnd(null, "Connection closed", emptyMap())) return gameEnd.await() } @@ -195,10 +195,10 @@ suspend fun gameMain(side: GlobalSide, token: String, state: GameState) { val connectionJob = async { gameConnection.execute(token) } val renderingJob = launch { gameRendering.execute(this@coroutineScope) } - val (finalWinner, finalMessage) = connectionJob.await() + val (finalWinner, finalMessage, finalSubplots) = connectionJob.await() renderingJob.cancel() interruptExit = false - Popup.GameOver(finalWinner, finalMessage, gameState.value).display() + Popup.GameOver(finalWinner, finalMessage, finalSubplots, gameState.value).display() } } diff --git a/src/jsMain/kotlin/starshipfights/game/client_training.kt b/src/jsMain/kotlin/starshipfights/game/client_training.kt index a4bacf8..b6f178c 100644 --- a/src/jsMain/kotlin/starshipfights/game/client_training.kt +++ b/src/jsMain/kotlin/starshipfights/game/client_training.kt @@ -51,7 +51,7 @@ class GameSession(gameState: GameState) { } } -private suspend fun GameNetworkInteraction.execute(): Pair { +private suspend fun GameNetworkInteraction.execute(): GameEvent.GameEnd { val gameSession = GameSession(gameState.value) val aiSide = mySide.other @@ -113,7 +113,7 @@ private suspend fun GameNetworkInteraction.execute(): Pair { aiHandlingJob.cancel() playerHandlingJob.cancel() - gameEnd.winner?.relativeTo(mySide) to gameEnd.message + gameEnd } } @@ -135,10 +135,10 @@ suspend fun trainingMain(state: GameState) { val connectionJob = async { gameConnection.execute() } val renderingJob = launch { gameRendering.execute(this@coroutineScope) } - val (finalWinner, finalMessage) = connectionJob.await() + val (finalWinner, finalMessage, finalSubplots) = connectionJob.await() renderingJob.cancel() interruptExit = false - Popup.GameOver(finalWinner, finalMessage, gameState.value).display() + Popup.GameOver(finalWinner, finalMessage, finalSubplots, gameState.value).display() } } diff --git a/src/jsMain/kotlin/starshipfights/game/game_ui.kt b/src/jsMain/kotlin/starshipfights/game/game_ui.kt index 8b1be0a..d18225e 100644 --- a/src/jsMain/kotlin/starshipfights/game/game_ui.kt +++ b/src/jsMain/kotlin/starshipfights/game/game_ui.kt @@ -31,6 +31,8 @@ object GameUI { private lateinit var topMiddleInfo: HTMLDivElement private lateinit var topRightBar: HTMLDivElement + private lateinit var objectives: HTMLDivElement + private lateinit var errorMessages: HTMLParagraphElement private lateinit var helpMessages: HTMLParagraphElement @@ -79,6 +81,10 @@ object GameUI { id = "top-right-bar" } + div { + id = "objectives" + } + p { id = "error-messages" } @@ -114,6 +120,8 @@ object GameUI { topMiddleInfo = document.getElementById("top-middle-info").unsafeCast() topRightBar = document.getElementById("top-right-bar").unsafeCast() + objectives = document.getElementById("objectives").unsafeCast() + errorMessages = document.getElementById("error-messages").unsafeCast() helpMessages = document.getElementById("help-messages").unsafeCast() @@ -394,6 +402,20 @@ object GameUI { } }.lastOrNull()?.scrollIntoView() + objectives.clear() + objectives.append { + for (objective in state.objectives(mySide)) { + val classes = when (objective.succeeded) { + true -> "item succeeded" + false -> "item failed" + else -> "item" + } + div(classes = classes) { + +objective.displayText + } + } + } + val abilities = state.getPossibleAbilities(mySide) topMiddleInfo.clear() diff --git a/src/jsMain/kotlin/starshipfights/game/popup.kt b/src/jsMain/kotlin/starshipfights/game/popup.kt index 70b58ea..9ed0a7c 100644 --- a/src/jsMain/kotlin/starshipfights/game/popup.kt +++ b/src/jsMain/kotlin/starshipfights/game/popup.kt @@ -497,17 +497,13 @@ sealed class Popup { } } - class GameOver(private val winner: LocalSide?, private val outcome: String, private val finalState: GameState) : Popup() { + class GameOver(private val winner: GlobalSide?, private val outcome: String, private val subplotStatuses: Map, private val finalState: GameState) : Popup() { override fun TagConsumer<*>.render(context: CoroutineContext, callback: (Nothing) -> Unit) { p { style = "text-align:center" strong(classes = "heading") { - +when (winner) { - LocalSide.GREEN -> "Victory" - LocalSide.RED -> "Defeat" - null -> "Stalemate" - } + +victoryTitle(mySide, winner, subplotStatuses) } } p { diff --git a/src/jsMain/resources/style.css b/src/jsMain/resources/style.css index 8ffb9ea..4bde652 100644 --- a/src/jsMain/resources/style.css +++ b/src/jsMain/resources/style.css @@ -219,15 +219,6 @@ hr + hr { display: none; } -#bottom-center-bar { - position: fixed; - bottom: 22.5vh; - left: 50vw; - width: 25vw; - height: 5vh; - transform: translate(-50%, 0); -} - input[type=text] { border: none; border-bottom: 2px solid transparent; @@ -375,3 +366,38 @@ button:disabled > img { color: #d22; font-weight: bold; } + +#objectives { + position: fixed; + top: 2.5vh; + left: 2.5vw; + width: 25vw; + height: 75vh; + font-size: 1.5em; +} + +#objectives .item { + background-color: rgba(0, 0, 0, 0.6); + color: #cccccc; + padding: 1.2em 0.4em 1.2em 2em; +} + +#objectives .item.failed { + color: #ff5555; +} + +#objectives .item.failed::before { + content: '✘'; + position: absolute; + left: 0.5em; +} + +#objectives .item.succeeded { + color: #55ff55; +} + +#objectives .item.succeeded::before { + content: '✔'; + position: absolute; + left: 0.5em; +} diff --git a/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt b/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt index ee61855..40bcce5 100644 --- a/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt +++ b/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt @@ -83,6 +83,7 @@ suspend fun generateTrainingInitialState(playerInfo: InGameAdmiral, enemyFaction faction = aiAdmiral.faction, rank = aiAdmiral.rank ), - battleInfo = battleInfo + battleInfo = battleInfo, + subplots = generateSubplots(battleInfo.size, GlobalSide.HOST) ) } diff --git a/src/jvmMain/kotlin/starshipfights/game/server_game.kt b/src/jvmMain/kotlin/starshipfights/game/server_game.kt index ff96b3f..a63d02b 100644 --- a/src/jvmMain/kotlin/starshipfights/game/server_game.kt +++ b/src/jvmMain/kotlin/starshipfights/game/server_game.kt @@ -32,7 +32,8 @@ object GameManager { start = generateGameStart(hostInfo, guestInfo, battleInfo), hostInfo = hostInfo, guestInfo = guestInfo, - battleInfo = battleInfo + battleInfo = battleInfo, + subplots = generateSubplots(battleInfo.size, GlobalSide.HOST) + generateSubplots(battleInfo.size, GlobalSide.GUEST) ) val session = GameSession(gameState) -- 2.25.1