Add Initiative mechanic
authorTheSaminator <TheSaminator@users.noreply.github.com>
Thu, 26 May 2022 21:22:10 +0000 (17:22 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Thu, 26 May 2022 21:22:10 +0000 (17:22 -0400)
src/commonMain/kotlin/starshipfights/game/game_ability.kt
src/commonMain/kotlin/starshipfights/game/game_initiative.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/game_state.kt
src/commonMain/kotlin/starshipfights/game/ship_modules.kt
src/commonMain/kotlin/starshipfights/game/ship_weapons.kt
src/jsMain/kotlin/starshipfights/game/client_game.kt
src/jsMain/kotlin/starshipfights/game/game_ui.kt
src/jsMain/kotlin/starshipfights/game/popup.kt

index 89bf7c13c74c52978be8f198ceb68df66e1636ae..815cb274019815f94c6a33876905aae23c529461 100644 (file)
@@ -41,7 +41,7 @@ sealed class PlayerAbilityType {
        data class DeployShip(val ship: Id<Ship>) : 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<ShipInstance>) : 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<ShipInstance>) : 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<ShipInstance>) : 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<ShipInstance>, override val weapon: Id<ShipWeapon>) : 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<ShipInstance>, override val weapon: Id<ShipWeapon>) : 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<ShipInstance>, override val weapon: Id<ShipWeapon>) : 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<ShipInstance>) : 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<ShipInstance>, 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<ShipInstance>) : 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<ShipInstance>) : 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<PlayerAbilityType> = if (ready == forPlayer)
+fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List<PlayerAbilityType> = 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 (file)
index 0000000..25fd6fd
--- /dev/null
@@ -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<GlobalSide, Double>) : 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<GlobalSide, Double>) = 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<ShipInstance>): Boolean {
+       val shipInstance = ships[ship] ?: return false
+       return currentInitiative != shipInstance.owner.other
+}
+
+fun GameState.canShipAttack(ship: Id<ShipInstance>): Boolean {
+       val shipInstance = ships[ship] ?: return false
+       return currentInitiative != shipInstance.owner.other
+}
index 2ed05d340b7af81b38d2ad0edbe2ab98ac4675c5..eb64f21973098fb3f75ed08d597b53d9a01ae2dc 100644 (file)
@@ -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<Id<ShipInstance>, ShipInstance> = emptyMap(),
        val destroyedShips: Map<Id<ShipInstance>, ShipWreck> = emptyMap(),
@@ -25,6 +26,9 @@ data class GameState(
        fun getShipOwner(id: Id<ShipInstance>) = 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<ChatEntry>()
+       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<ShipHangarWing, Double>()
@@ -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
index 45172d0ca26f4f430575ceca166132d2fe981f20..5ad79f38de46f505a59fa0dc89e348edd54262d7 100644 (file)
@@ -56,9 +56,9 @@ enum class ShipModuleStatus(val canBeUsed: Boolean, val canBeRepaired: Boolean)
 data class ShipModulesStatus(val statuses: Map<ShipModule, ShipModuleStatus>) {
        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()
        )
        
index bd08193122c406d32683cbc9676f47b3be7670b2..464dfd0927a77db114928872b5192f4f1c7fc384 100644 (file)
@@ -538,6 +538,9 @@ fun ImpactResult.applyCriticals(attacker: ShipInstance, weaponId: Id<ShipWeapon>
        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
index 2c2a61e3463f81b41a71fbaa42f0ed4e8b287c7a..a92e81f0c8ef39e26070ddcdbde2e35e47d3753c 100644 (file)
@@ -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)
+                                       }
                        }
                }
        }
index 75a37f5ae43d3a301bdf200331227573c86a650c..197d24e0107e4cd9837f87059a12ea91e6e6fb2f 100644 (file)
@@ -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") {
index a77b9328785a31e3e5d153cffd3a4c6507990e1e..d342307a719bf87736c0a48cf2357ffa9469772d 100644 (file)
@@ -73,7 +73,7 @@ sealed class Popup<out T> {
                        }
                        
                        p {
-                               style = "text-alin:center"
+                               style = "text-align:center"
                                
                                +"Select one of your admirals to continue:"
                        }
@@ -97,7 +97,7 @@ sealed class Popup<out T> {
                        }
                        
                        p {
-                               style = "text-alin:center"
+                               style = "text-align:center"
                                
                                +"Or return to "
                                a(href = "/me") { +"your user page" }