From 14278fc329070d1c5275b45f8d4dd20e50307a3a Mon Sep 17 00:00:00 2001 From: TheSaminator Date: Thu, 26 May 2022 17:22:10 -0400 Subject: [PATCH] Add Initiative mechanic --- .../starshipfights/game/game_ability.kt | 55 ++++++++++--- .../starshipfights/game/game_initiative.kt | 82 +++++++++++++++++++ .../kotlin/starshipfights/game/game_state.kt | 29 +++++-- .../starshipfights/game/ship_modules.kt | 6 +- .../starshipfights/game/ship_weapons.kt | 3 + .../kotlin/starshipfights/game/client_game.kt | 16 ++-- .../kotlin/starshipfights/game/game_ui.kt | 12 +++ .../kotlin/starshipfights/game/popup.kt | 4 +- 8 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 src/commonMain/kotlin/starshipfights/game/game_initiative.kt diff --git a/src/commonMain/kotlin/starshipfights/game/game_ability.kt b/src/commonMain/kotlin/starshipfights/game/game_ability.kt index 89bf7c1..815cb27 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_ability.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_ability.kt @@ -41,7 +41,7 @@ sealed class PlayerAbilityType { data class DeployShip(val ship: Id) : PlayerAbilityType() { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase != GamePhase.Deploy) return null - if (gameState.ready == playerSide) return null + if (gameState.doneWithPhase == playerSide) return null val pickBoundary = gameState.start.playerStart(playerSide).deployZone val playerStart = gameState.start.playerStart(playerSide) @@ -96,7 +96,7 @@ sealed class PlayerAbilityType { @Serializable data class UndeployShip(val ship: Id) : PlayerAbilityType() { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { - return if (gameState.phase == GamePhase.Deploy && gameState.ready != playerSide) PlayerAbilityData.UndeployShip else null + return if (gameState.phase == GamePhase.Deploy && gameState.doneWithPhase != playerSide) PlayerAbilityData.UndeployShip else null } override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { @@ -194,6 +194,8 @@ sealed class PlayerAbilityType { data class MoveShip(override val ship: Id) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Move) return null + if (!gameState.canShipMove(ship)) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.isDoneCurrentPhase) return null @@ -231,6 +233,7 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (data !is PlayerAbilityData.MoveShip) return GameEvent.InvalidAction("Internal error from using player ability") + if (!gameState.canShipMove(ship)) return GameEvent.InvalidAction("You do not have the initiative") val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.isDoneCurrentPhase) return GameEvent.InvalidAction("Ships cannot be moved twice") @@ -296,7 +299,7 @@ sealed class PlayerAbilityType { ships = newShips, destroyedShips = newWrecks, chatBox = newChatEntries, - ) + ).withRecalculatedInitiative { calculateMovePhaseInitiative() } ) } } @@ -305,8 +308,11 @@ sealed class PlayerAbilityType { data class UseInertialessDrive(override val ship: Id) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Move) return null + if (!gameState.canShipMove(ship)) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.isDoneCurrentPhase) return null + if (!shipInstance.canUseInertialessDrive) return null val movement = shipInstance.movement if (movement !is FelinaeShipMovement) return null @@ -327,6 +333,7 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (data !is PlayerAbilityData.UseInertialessDrive) return GameEvent.InvalidAction("Internal error from using player ability") + if (!gameState.canShipMove(ship)) return GameEvent.InvalidAction("You do not have the initiative") val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.isDoneCurrentPhase) return GameEvent.InvalidAction("Ships cannot be moved twice") @@ -392,7 +399,7 @@ sealed class PlayerAbilityType { ships = newShips, destroyedShips = newWrecks, chatBox = newChatEntries, - ) + ).withRecalculatedInitiative { calculateMovePhaseInitiative() } ) } } @@ -401,9 +408,11 @@ sealed class PlayerAbilityType { data class ChargeLance(override val ship: Id, override val weapon: Id) : PlayerAbilityType(), CombatAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Attack) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.weaponAmount <= 0) return null if (weapon in shipInstance.usedArmaments) return null + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null if (shipWeapon !is ShipWeaponInstance.Lance) return null @@ -412,9 +421,11 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only charge lances during Phase III") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.weaponAmount <= 0) return GameEvent.InvalidAction("Not enough power to charge lances") if (weapon in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot charge used lances") + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist") if (shipWeapon !is ShipWeaponInstance.Lance) return GameEvent.InvalidAction("Cannot charge non-lance weapons") @@ -439,8 +450,10 @@ sealed class PlayerAbilityType { data class UseWeapon(override val ship: Id, override val weapon: Id) : PlayerAbilityType(), CombatAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Attack) return null + val shipInstance = gameState.ships[ship] ?: return null if (!shipInstance.canUseWeapon(weapon)) return null + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null val pickResponse = pick(shipInstance.getWeaponPickRequest(shipWeapon.weapon)) @@ -452,8 +465,10 @@ sealed class PlayerAbilityType { if (data !is PlayerAbilityData.UseWeapon) return GameEvent.InvalidAction("Internal error from using player ability") if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only attack during Phase III") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That attacking ship does not exist") if (!shipInstance.canUseWeapon(weapon)) return GameEvent.InvalidAction("That weapon cannot be used") + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist") val pickRequest = shipInstance.getWeaponPickRequest(shipWeapon.weapon) @@ -469,8 +484,10 @@ sealed class PlayerAbilityType { data class RecallStrikeCraft(override val ship: Id, override val weapon: Id) : PlayerAbilityType(), CombatAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Attack) return null + val shipInstance = gameState.ships[ship] ?: return null if (weapon !in shipInstance.usedArmaments) return null + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null if (shipWeapon !is ShipWeaponInstance.Hangar) return null @@ -479,8 +496,10 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only recall strike craft during Phase III") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (weapon !in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot recall unused strike craft") + val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist") if (shipWeapon !is ShipWeaponInstance.Hangar) return GameEvent.InvalidAction("Cannot recall non-hangar weapons") @@ -507,6 +526,7 @@ sealed class PlayerAbilityType { data class DisruptionPulse(override val ship: Id) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Attack) return null + val shipInstance = gameState.ships[ship] ?: return null if (!shipInstance.canUseDisruptionPulse) return null if (shipInstance.hasUsedDisruptionPulse) return null @@ -516,6 +536,7 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only emit Disruption Pulses during Phase III") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (!shipInstance.canUseDisruptionPulse) return GameEvent.InvalidAction("Cannot use Disruption Pulse") if (shipInstance.hasUsedDisruptionPulse) return GameEvent.InvalidAction("Cannot use Disruption Pulse twice") @@ -562,6 +583,7 @@ sealed class PlayerAbilityType { data class RepairShipModule(override val ship: Id, val module: ShipModule) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Repair) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.durability !is StandardShipDurability) return null if (shipInstance.remainingRepairTokens <= 0) return null @@ -572,6 +594,7 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only repair modules during Phase IV") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.durability !is StandardShipDurability) return GameEvent.InvalidAction("That ship cannot manually repair subsystems") if (shipInstance.remainingRepairTokens <= 0) return GameEvent.InvalidAction("That ship has no remaining repair tokens") @@ -596,6 +619,7 @@ sealed class PlayerAbilityType { data class ExtinguishFire(override val ship: Id) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Repair) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.durability !is StandardShipDurability) return null if (shipInstance.remainingRepairTokens <= 0) return null @@ -606,6 +630,7 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only extinguish fires during Phase IV") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.durability !is StandardShipDurability) return GameEvent.InvalidAction("That ship cannot manually extinguish fires") if (shipInstance.remainingRepairTokens <= 0) return GameEvent.InvalidAction("That ship has no remaining repair tokens") @@ -630,6 +655,7 @@ sealed class PlayerAbilityType { data class Recoalesce(override val ship: Id) : PlayerAbilityType(), ShipAbility { override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? { if (gameState.phase !is GamePhase.Repair) return null + val shipInstance = gameState.ships[ship] ?: return null if (shipInstance.durability !is FelinaeShipDurability) return null if (!shipInstance.canUseRecoalescence) return null @@ -639,11 +665,16 @@ sealed class PlayerAbilityType { override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent { if (gameState.phase !is GamePhase.Repair) return GameEvent.InvalidAction("Ships can only extinguish fires during Phase IV") + val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist") if (shipInstance.durability !is FelinaeShipDurability) return GameEvent.InvalidAction("That ship cannot recoalesce its hull") if (!shipInstance.canUseRecoalescence) return GameEvent.InvalidAction("That ship is not in Recoalescence mode") - val newHullAmount = Random.nextInt(shipInstance.hullAmount, shipInstance.durability.maxHullPoints) + val newHullAmountRange = (shipInstance.hullAmount + 1) until shipInstance.durability.maxHullPoints + val (newHullAmount, newMaxHullDamage) = if (newHullAmountRange.isEmpty()) + shipInstance.hullAmount to shipInstance.recoalescenceMaxHullDamage + else + newHullAmountRange.random() to (shipInstance.recoalescenceMaxHullDamage + 1) val repairs = shipInstance.modulesStatus.statuses.filterValues { it == ShipModuleStatus.DAMAGED || it == ShipModuleStatus.DESTROYED @@ -652,12 +683,12 @@ sealed class PlayerAbilityType { var newModules = shipInstance.modulesStatus for (repair in repairs) { if (Random.nextBoolean()) - newModules = newModules.repair(repair) + newModules = newModules.repair(repair, repairUnrepairable = true) } val newShip = shipInstance.copy( hullAmount = newHullAmount, - recoalescenceMaxHullDamage = shipInstance.recoalescenceMaxHullDamage + 1, + recoalescenceMaxHullDamage = newMaxHullDamage, modulesStatus = newModules, isDoneCurrentPhase = true ) @@ -718,7 +749,7 @@ sealed class PlayerAbilityData { object Recoalesce : PlayerAbilityData() } -fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List = if (ready == forPlayer) +fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List = if (doneWithPhase == forPlayer) emptyList() else when (phase) { GamePhase.Deploy -> { @@ -764,11 +795,13 @@ else when (phase) { } is GamePhase.Move -> { val movableShips = ships + .filterKeys { canShipMove(it) } .filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase } .keys .map { PlayerAbilityType.MoveShip(it) } val inertialessShips = ships + .filterKeys { canShipMove(it) } .filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase && it.canUseInertialessDrive } .keys .map { PlayerAbilityType.UseInertialessDrive(it) } @@ -794,6 +827,7 @@ else when (phase) { } val usableWeapons = ships + .filterKeys { canShipAttack(it) } .filterValues { it.owner == forPlayer } .flatMap { (id, ship) -> ship.armaments.weaponInstances.keys.mapNotNull { weaponId -> @@ -804,6 +838,7 @@ else when (phase) { } val usableDisruptionPulses = ships + .filterKeys { canShipAttack(it) } .filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase && it.canUseDisruptionPulse } .keys .map { PlayerAbilityType.DisruptionPulse(it) } @@ -840,7 +875,7 @@ else when (phase) { PlayerAbilityType.ExtinguishFire(it) } - val recoalescence = ships + val recoalescibleShips = ships .filterValues { it.owner == forPlayer && it.canUseRecoalescence } .keys .map { @@ -851,7 +886,7 @@ else when (phase) { listOf(PlayerAbilityType.DonePhase(GamePhase.Repair(phase.turn))) else emptyList() - repairableModules + extinguishableFires + recoalescence + finishRepairing + repairableModules + extinguishableFires + recoalescibleShips + finishRepairing } } diff --git a/src/commonMain/kotlin/starshipfights/game/game_initiative.kt b/src/commonMain/kotlin/starshipfights/game/game_initiative.kt new file mode 100644 index 0000000..25fd6fd --- /dev/null +++ b/src/commonMain/kotlin/starshipfights/game/game_initiative.kt @@ -0,0 +1,82 @@ +package starshipfights.game + +import kotlinx.serialization.Serializable +import starshipfights.data.Id + +@Serializable +data class InitiativePair( + val hostSide: Double, + val guestSide: Double +) { + constructor(map: Map) : this( + map[GlobalSide.HOST] ?: 0.0, + map[GlobalSide.GUEST] ?: 0.0, + ) + + operator fun get(side: GlobalSide) = when (side) { + GlobalSide.HOST -> hostSide + GlobalSide.GUEST -> guestSide + } + + fun copy(map: Map) = copy( + hostSide = map[GlobalSide.HOST] ?: hostSide, + guestSide = map[GlobalSide.GUEST] ?: guestSide, + ) +} + +fun GameState.calculateMovePhaseInitiative(): InitiativePair = InitiativePair( + ships + .values + .groupBy { it.owner } + .mapValues { (_, shipList) -> + shipList + .filter { !it.isDoneCurrentPhase } + .sumOf { it.ship.pointCost * it.movementCoefficient } + } +) + +fun GameState.calculateAttackPhaseInitiative(): InitiativePair = InitiativePair( + ships + .values + .groupBy { it.owner } + .mapValues { (_, shipList) -> + shipList + .filter { !it.isDoneCurrentPhase } + .sumOf { ship -> + val allWeapons = ship.armaments.weaponInstances + .filterValues { weaponInstance -> + ships.values.any { target -> + target.position.location in ship.getWeaponPickRequest(weaponInstance.weapon).boundary + } + } + val usableWeapons = allWeapons - ship.usedArmaments + + val allWeaponShots = allWeapons.values.sumOf { it.weapon.numShots } + val usableWeaponShots = usableWeapons.values.sumOf { it.weapon.numShots } + + ship.ship.pointCost * (usableWeaponShots.toDouble() / allWeaponShots) + } + } +) + +fun GameState.withRecalculatedInitiative(initiativePairAccessor: GameState.() -> InitiativePair): GameState { + val initiativePair = initiativePairAccessor() + + return copy( + calculatedInitiative = when { + initiativePair.hostSide > initiativePair.guestSide -> GlobalSide.HOST + initiativePair.hostSide < initiativePair.guestSide -> GlobalSide.GUEST + else -> calculatedInitiative?.other + } + ) +} + +fun GameState.canShipMove(ship: Id): Boolean { + val shipInstance = ships[ship] ?: return false + return currentInitiative != shipInstance.owner.other +} + +fun GameState.canShipAttack(ship: Id): Boolean { + val shipInstance = ships[ship] ?: return false + return currentInitiative != shipInstance.owner.other +} diff --git a/src/commonMain/kotlin/starshipfights/game/game_state.kt b/src/commonMain/kotlin/starshipfights/game/game_state.kt index 2ed05d3..eb64f21 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_state.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_state.kt @@ -14,7 +14,8 @@ data class GameState( val battleInfo: BattleInfo, val phase: GamePhase = GamePhase.Deploy, - val ready: GlobalSide? = null, + val doneWithPhase: GlobalSide? = null, + val calculatedInitiative: GlobalSide? = null, val ships: Map, ShipInstance> = emptyMap(), val destroyedShips: Map, ShipWreck> = emptyMap(), @@ -25,6 +26,9 @@ data class GameState( fun getShipOwner(id: Id) = destroyedShips[id]?.owner ?: ships.getValue(id).owner } +val GameState.currentInitiative: GlobalSide? + get() = calculatedInitiative?.takeIf { it != doneWithPhase } + fun GameState.canFinishPhase(side: GlobalSide): Boolean { return when (phase) { GamePhase.Deploy -> { @@ -42,8 +46,13 @@ private fun GameState.afterPhase(): GameState { var newShips = ships val newWrecks = destroyedShips.toMutableMap() val newChatEntries = mutableListOf() + var newInitiative: GameState.() -> InitiativePair = { InitiativePair(emptyMap()) } when (phase) { + is GamePhase.Power -> { + // Prepare for move phase + newInitiative = { calculateMovePhaseInitiative() } + } is GamePhase.Move -> { // Set velocity to 0 for halted ships newShips = newShips.mapValues { (_, ship) -> @@ -58,6 +67,9 @@ private fun GameState.afterPhase(): GameState { ship.copy(usedInertialessDriveShots = ship.usedInertialessDriveShots - 1) else ship } + + // Prepare for attack phase + newInitiative = { calculateAttackPhaseInitiative() } } is GamePhase.Attack -> { val strikeWingDamage = mutableMapOf() @@ -122,13 +134,20 @@ private fun GameState.afterPhase(): GameState { } } - return copy(phase = phase.next(), ships = newShips.mapValues { (_, ship) -> ship.copy(isDoneCurrentPhase = false) }, destroyedShips = newWrecks, chatBox = chatBox + newChatEntries) + return copy( + phase = phase.next(), + ships = newShips.mapValues { (_, ship) -> + ship.copy(isDoneCurrentPhase = false) + }, + destroyedShips = newWrecks, + chatBox = chatBox + newChatEntries + ).withRecalculatedInitiative(newInitiative) } -fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (ready == playerSide.other) { - afterPhase().copy(ready = null) +fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (doneWithPhase == playerSide.other) { + afterPhase().copy(doneWithPhase = null) } else - copy(ready = playerSide) + copy(doneWithPhase = playerSide) private fun GameState.victoryMessage(winner: GlobalSide): String { val winnerName = admiralInfo(winner).fullName diff --git a/src/commonMain/kotlin/starshipfights/game/ship_modules.kt b/src/commonMain/kotlin/starshipfights/game/ship_modules.kt index 45172d0..5ad79f3 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_modules.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_modules.kt @@ -56,9 +56,9 @@ enum class ShipModuleStatus(val canBeUsed: Boolean, val canBeRepaired: Boolean) data class ShipModulesStatus(val statuses: Map) { operator fun get(module: ShipModule) = statuses[module] ?: ShipModuleStatus.ABSENT - fun repair(module: ShipModule) = ShipModulesStatus( - statuses + if (this[module].canBeRepaired) - mapOf(module to ShipModuleStatus.INTACT) + fun repair(module: ShipModule, repairUnrepairable: Boolean = false) = ShipModulesStatus( + statuses + if (this[module].canBeRepaired || (repairUnrepairable && !this[module].canBeUsed)) + mapOf(module to ShipModuleStatus.values()[this[module].ordinal - 1]) else emptyMap() ) diff --git a/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt b/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt index bd08193..464dfd0 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt @@ -538,6 +538,9 @@ fun ImpactResult.applyCriticals(attacker: ShipInstance, weaponId: Id return when (this) { is ImpactResult.Destroyed -> this is ImpactResult.Damaged -> { + if (damage is ImpactDamage.Failed) + return this + val critChance = criticalChance(attacker, weaponId, ship) if (Random.nextDouble() > critChance) this diff --git a/src/jsMain/kotlin/starshipfights/game/client_game.kt b/src/jsMain/kotlin/starshipfights/game/client_game.kt index 2c2a61e..a92e81f 100644 --- a/src/jsMain/kotlin/starshipfights/game/client_game.kt +++ b/src/jsMain/kotlin/starshipfights/game/client_game.kt @@ -100,22 +100,16 @@ private suspend fun GameRenderInteraction.execute(scope: CoroutineScope) { } launch { - val doneDeploying = Job() - - launch { - doneDeploying.join() - - val pickContext = pickContextDeferred.await() - beginSelecting(pickContext) - handleSelections(pickContext) - } - gameState.collect { state -> GameRender.renderGameState(scene, state) GameUI.drawGameUI(state) if (state.phase != GamePhase.Deploy) - doneDeploying.complete() + launch { + val pickContext = pickContextDeferred.await() + beginSelecting(pickContext) + handleSelections(pickContext) + } } } } diff --git a/src/jsMain/kotlin/starshipfights/game/game_ui.kt b/src/jsMain/kotlin/starshipfights/game/game_ui.kt index 75a37f5..197d24e 100644 --- a/src/jsMain/kotlin/starshipfights/game/game_ui.kt +++ b/src/jsMain/kotlin/starshipfights/game/game_ui.kt @@ -390,6 +390,12 @@ object GameUI { } br +"Phase II - Ship Movement" + br + +if (state.doneWithPhase == mySide) + "You have ended your phase" + else if (state.currentInitiative != mySide.other) + "You have the initiative!" + else "Your opponent has the initiative" } is GamePhase.Attack -> { strong(classes = "heading") { @@ -397,6 +403,12 @@ object GameUI { } br +"Phase III - Weapons Fire" + br + +if (state.doneWithPhase == mySide) + "You have ended your phase" + else if (state.currentInitiative != mySide.other) + "You have the initiative!" + else "Your opponent has the initiative" } is GamePhase.Repair -> { strong(classes = "heading") { diff --git a/src/jsMain/kotlin/starshipfights/game/popup.kt b/src/jsMain/kotlin/starshipfights/game/popup.kt index a77b932..d342307 100644 --- a/src/jsMain/kotlin/starshipfights/game/popup.kt +++ b/src/jsMain/kotlin/starshipfights/game/popup.kt @@ -73,7 +73,7 @@ sealed class Popup { } p { - style = "text-alin:center" + style = "text-align:center" +"Select one of your admirals to continue:" } @@ -97,7 +97,7 @@ sealed class Popup { } p { - style = "text-alin:center" + style = "text-align:center" +"Or return to " a(href = "/me") { +"your user page" } -- 2.25.1