So many changes and fixes this might as well be called Starship Fights 3
authorTheSaminator <TheSaminator@users.noreply.github.com>
Sun, 13 Mar 2022 21:50:33 +0000 (17:50 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Sun, 13 Mar 2022 21:50:33 +0000 (17:50 -0400)
15 files changed:
src/commonMain/kotlin/starshipfights/game/admiralty.kt
src/commonMain/kotlin/starshipfights/game/game_ability.kt
src/commonMain/kotlin/starshipfights/game/game_state.kt
src/commonMain/kotlin/starshipfights/game/ship.kt
src/commonMain/kotlin/starshipfights/game/ship_instances.kt
src/commonMain/kotlin/starshipfights/game/ship_modules.kt
src/commonMain/kotlin/starshipfights/game/ship_weapons.kt
src/commonMain/kotlin/starshipfights/game/util.kt
src/jsMain/kotlin/starshipfights/game/game_ui.kt
src/jsMain/resources/style.css
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt
src/jvmMain/kotlin/starshipfights/data/data_connection.kt
src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt
src/jvmMain/kotlin/starshipfights/info/views_ships.kt

index acdafeab1751a4b58365676d5bc8060a6568c5e8..6376e0509ac8b7be13768a7f7f92e253137c38a7 100644 (file)
@@ -41,7 +41,7 @@ enum class AdmiralRank {
                }
        
        companion object {
-               fun fromAcumen(acumen: Int) = values().firstOrNull { it.minAcumen <= acumen } ?: REAR_ADMIRAL
+               fun fromAcumen(acumen: Int) = values().lastOrNull { it.minAcumen <= acumen } ?: values().first()
        }
 }
 
index 5102bcc4ea7a783da8f114dce41a080118afd14b..50a791c37ec3ba20caadc8cfb8becd4ea97f6c3f 100644 (file)
@@ -2,6 +2,7 @@ package starshipfights.game
 
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
+import kotlin.math.abs
 
 sealed interface ShipAbility {
        val ship: Id<ShipInstance>
@@ -220,10 +221,46 @@ sealed class PlayerAbilityType {
                        if (data.newPosition.location.distanceToLineSegment(moveFrom, moveTo) > EPSILON) return GameEvent.InvalidAction("Illegal move - must be on facing line")
                        
                        val newShipInstance = shipInstance.copy(position = data.newPosition, isDoneCurrentPhase = true)
-                       val newShips = gameState.ships + mapOf(ship to newShipInstance)
+                       
+                       // Identify enemy ships
+                       val identifiedEnemyShips = gameState.ships.filterValues { enemyShip ->
+                               !enemyShip.isIdentified && enemyShip.owner != playerSide && (enemyShip.position.location - newShipInstance.position.location).length <= SHIP_SENSOR_RANGE
+                       }
+                       
+                       // Be identified by enemy ships
+                       val shipsToBeIdentified = identifiedEnemyShips + if (!newShipInstance.isIdentified && identifiedEnemyShips.isNotEmpty())
+                               mapOf(ship to newShipInstance)
+                       else emptyMap()
+                       
+                       val identifiedShips = shipsToBeIdentified.mapValues { (_, shipInstance) -> shipInstance.copy(isIdentified = true) }
+                       
+                       // Ships that move off the battlefield are considered to disengage
+                       val isDisengaged = newShipInstance.position.location.vector.let { (x, y) ->
+                               val mx = gameState.start.battlefieldWidth / 2
+                               val my = gameState.start.battlefieldLength / 2
+                               abs(x) > mx || abs(y) > my
+                       }
+                       
+                       val newChatEntries = gameState.chatBox + identifiedShips.map { (id, _) ->
+                               ChatEntry.ShipIdentified(id, Moment.now)
+                       } + (if (isDisengaged)
+                               listOf(ChatEntry.ShipEscaped(ship, Moment.now))
+                       else emptyList())
+                       
+                       val newShips = (gameState.ships + mapOf(ship to newShipInstance) + identifiedShips) - (if (isDisengaged)
+                               setOf(ship)
+                       else emptySet())
+                       
+                       val newWrecks = gameState.destroyedShips + (if (isDisengaged)
+                               mapOf(ship to ShipWreck(newShipInstance.ship, newShipInstance.owner, true))
+                       else emptyMap())
                        
                        return GameEvent.StateChange(
-                               gameState.copy(ships = newShips)
+                               gameState.copy(
+                                       ships = newShips,
+                                       destroyedShips = newWrecks,
+                                       chatBox = newChatEntries,
+                               )
                        )
                }
        }
@@ -256,7 +293,7 @@ sealed class PlayerAbilityType {
                                                        weaponAmount = shipInstance.weaponAmount - 1,
                                                        armaments = shipInstance.armaments.copy(
                                                                weaponInstances = shipInstance.armaments.weaponInstances + mapOf(
-                                                                       weapon to shipWeapon.copy(numCharges = shipWeapon.numCharges + 1)
+                                                                       weapon to shipWeapon.copy(numCharges = shipWeapon.numCharges + shipInstance.firepower.lanceCharging)
                                                                )
                                                        )
                                                )
@@ -274,7 +311,7 @@ sealed class PlayerAbilityType {
                        if (!shipInstance.canUseWeapon(weapon)) return null
                        val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
                        
-                       val pickResponse = pick(getWeaponPickRequest(shipWeapon.weapon, shipInstance.position, shipInstance.owner))
+                       val pickResponse = pick(shipInstance.getWeaponPickRequest(shipWeapon.weapon, shipInstance.position, shipInstance.owner))
                        
                        return pickResponse?.let { PlayerAbilityData.UseWeapon(it) }
                }
@@ -287,7 +324,7 @@ sealed class PlayerAbilityType {
                        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 = getWeaponPickRequest(shipWeapon.weapon, shipInstance.position, shipInstance.owner)
+                       val pickRequest = shipInstance.getWeaponPickRequest(shipWeapon.weapon, shipInstance.position, shipInstance.owner)
                        val pickResponse = data.target
                        
                        if (!gameState.isValidPick(pickRequest, pickResponse)) return GameEvent.InvalidAction("Invalid target")
@@ -486,7 +523,7 @@ else when (phase) {
                                ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
                                        PlayerAbilityType.ChargeLance(id, weaponId).takeIf {
                                                when (weapon) {
-                                                       is ShipWeaponInstance.Lance -> weapon.numCharges < 7 && weaponId !in ship.usedArmaments
+                                                       is ShipWeaponInstance.Lance -> weapon.numCharges < 7.0 && weaponId !in ship.usedArmaments
                                                        else -> false
                                                }
                                        }
index 555dfaa1ca15a15462d32e50e2068271c22d95e3..4f756061805c146a7b342aaa583544a7159797d5 100644 (file)
@@ -2,7 +2,6 @@ package starshipfights.game
 
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
-import kotlin.math.abs
 import kotlin.random.Random
 import kotlin.random.nextInt
 
@@ -46,77 +45,27 @@ private fun GameState.afterPhase(): GameState {
        val newChatEntries = mutableListOf<ChatEntry>()
        
        when (phase) {
-               is GamePhase.Move -> {
-                       // Ships that move off the battlefield are considered to disengage
-                       newShips = newShips.mapNotNull fleeingShips@{ (id, ship) ->
-                               val r = ship.position.location.vector
-                               val mx = start.battlefieldWidth / 2
-                               val my = start.battlefieldLength / 2
-                               
-                               if (abs(r.x) > mx || abs(r.y) > my) {
-                                       newWrecks[id] = ShipWreck(ship.ship, ship.owner, true)
-                                       newChatEntries += ChatEntry.ShipEscaped(id, Moment.now)
-                                       return@fleeingShips null
-                               }
-                               
-                               id to ship
-                       }.toMap()
-                       
-                       // Identify enemy ships
-                       newShips = newShips.mapValues { (_, ship) ->
-                               if (ship.isIdentified) ship
-                               else if (newShips.values.any { it.owner != ship.owner && (it.position.location - ship.position.location).length <= SHIP_SENSOR_RANGE })
-                                       ship.copy(isIdentified = true).also {
-                                               newChatEntries += ChatEntry.ShipIdentified(it.id, Moment.now)
-                                       }
-                               else ship
-                       }
-               }
                is GamePhase.Attack -> {
                        val strikeWingDamage = mutableMapOf<ShipHangarWing, Double>()
                        
                        // Apply damage to ships from strike craft
                        newShips = newShips.mapNotNull strikeBombard@{ (id, ship) ->
-                               if (ship.bomberWings.isEmpty())
-                                       return@strikeBombard id to ship
-                               
-                               val totalFighterHealth = ship.fighterWings.sumOf { (carrierId, wingId) ->
-                                       (newShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
-                               } + (if (ship.canUseTurrets) ship.ship.durability.turretDefense else 0.0)
-                               
-                               val totalBomberHealth = ship.bomberWings.sumOf { (carrierId, wingId) ->
-                                       (newShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
-                               }
-                               
-                               val maxBomberWingOutput = smoothNegative(totalBomberHealth - totalFighterHealth)
-                               val maxFighterWingOutput = smoothNegative(totalFighterHealth - totalBomberHealth)
-                               
-                               ship.fighterWings.forEach { strikeWingDamage[it] = Random.nextDouble() * maxBomberWingOutput }
-                               ship.bomberWings.forEach { strikeWingDamage[it] = Random.nextDouble() * maxFighterWingOutput }
-                               
-                               var hits = 0
-                               var chanceOfShipDamage = smoothNegative(maxBomberWingOutput - maxFighterWingOutput)
-                               while (chanceOfShipDamage >= 1.0) {
-                                       hits++
-                                       chanceOfShipDamage -= 1.0
-                               }
-                               if (Random.nextDouble() < chanceOfShipDamage)
-                                       hits++
-                               
-                               when (val impactResult = ship.impact(hits)) {
+                               when (val impact = ship.afterBombed(newShips, strikeWingDamage)) {
                                        is ImpactResult.Damaged -> {
-                                               newChatEntries += ChatEntry.ShipAttacked(
-                                                       ship = id,
-                                                       attacker = ShipAttacker.Bombers,
-                                                       sentAt = Moment.now,
-                                                       damageInflicted = hits,
-                                                       weapon = null,
-                                                       critical = null
-                                               )
-                                               id to impactResult.ship
+                                               impact.amount?.let { damage ->
+                                                       newChatEntries += ChatEntry.ShipAttacked(
+                                                               ship = id,
+                                                               attacker = ShipAttacker.Bombers,
+                                                               sentAt = Moment.now,
+                                                               damageInflicted = damage,
+                                                               weapon = null,
+                                                               critical = impact.critical.report()
+                                                       )
+                                               }
+                                               id to impact.ship
                                        }
                                        is ImpactResult.Destroyed -> {
-                                               newWrecks[id] = impactResult.ship
+                                               newWrecks[id] = impact.ship
                                                newChatEntries += ChatEntry.ShipDestroyed(id, Moment.now, ShipAttacker.Bombers)
                                                null
                                        }
@@ -124,30 +73,10 @@ private fun GameState.afterPhase(): GameState {
                        }.toMap()
                        
                        // Apply damage to strike craft wings
-                       newShips = newShips.mapValues { (shipId, ship) ->
-                               val newArmaments = ship.armaments.weaponInstances.mapValues { (weaponId, weapon) ->
-                                       if (weapon is ShipWeaponInstance.Hangar)
-                                               weapon.copy(wingHealth = weapon.wingHealth - (strikeWingDamage[ShipHangarWing(shipId, weaponId)] ?: 0.0))
-                                       else weapon
-                               }.filterValues { it !is ShipWeaponInstance.Hangar || it.wingHealth > 0.0 }
-                               
-                               ship.copy(
-                                       armaments = ShipInstanceArmaments(newArmaments)
-                               )
-                       }
-                       
-                       // Recall strike craft and regenerate weapon power
                        newShips = newShips.mapValues { (_, ship) ->
-                               ship.copy(
-                                       weaponAmount = ship.powerMode.weapons,
-                                       
-                                       fighterWings = emptySet(),
-                                       bomberWings = emptySet(),
-                                       usedArmaments = emptySet(),
-                               )
+                               ship.afterBombing(strikeWingDamage)
                        }
-               }
-               is GamePhase.Repair -> {
+                       
                        // Deal fire damage
                        newShips = newShips.mapNotNull fireDamage@{ (id, ship) ->
                                if (ship.numFires <= 0)
@@ -175,11 +104,16 @@ private fun GameState.afterPhase(): GameState {
                                }
                        }.toMap()
                        
-                       // Replenish repair tokens and regenerate shield power
+                       // Replenish repair tokens, recall strike craft, and regenerate weapons and shields power
                        newShips = newShips.mapValues { (_, ship) ->
                                ship.copy(
+                                       weaponAmount = ship.powerMode.weapons,
                                        shieldAmount = if (ship.canUseShields) (ship.shieldAmount..ship.powerMode.shields).random() else 0,
-                                       usedRepairTokens = 0
+                                       usedRepairTokens = 0,
+                                       
+                                       fighterWings = emptySet(),
+                                       bomberWings = emptySet(),
+                                       usedArmaments = emptySet(),
                                )
                        }
                }
index e18baa9f7eb6de4dd0d134c64d45f10f10cb73ef..60552bd788d8547d60a603e52c33e3b1b2597769 100644 (file)
@@ -26,6 +26,9 @@ data class Ship(
        val durability: ShipDurability
                get() = shipType.weightClass.durability
        
+       val firepower: ShipFirepower
+               get() = shipType.weightClass.firepower
+       
        val armaments: ShipArmaments
                get() = shipType.armaments
 }
@@ -44,7 +47,7 @@ val ShipWeightClass.reactor: ShipReactor
                ShipWeightClass.ESCORT -> ShipReactor(2, 1)
                ShipWeightClass.DESTROYER -> ShipReactor(3, 1)
                ShipWeightClass.CRUISER -> ShipReactor(4, 2)
-               ShipWeightClass.BATTLECRUISER -> ShipReactor(5, 3)
+               ShipWeightClass.BATTLECRUISER -> ShipReactor(6, 3)
                ShipWeightClass.BATTLESHIP -> ShipReactor(7, 4)
                
                ShipWeightClass.GRAND_CRUISER -> ShipReactor(6, 4)
@@ -97,20 +100,49 @@ data class ShipDurability(
 val ShipWeightClass.durability: ShipDurability
        get() = when (this) {
                ShipWeightClass.ESCORT -> ShipDurability(4, 0.5, 1)
-               ShipWeightClass.DESTROYER -> ShipDurability(8, 0.5, 2)
-               ShipWeightClass.CRUISER -> ShipDurability(12, 1.0, 3)
-               ShipWeightClass.BATTLECRUISER -> ShipDurability(14, 1.0, 3)
-               ShipWeightClass.BATTLESHIP -> ShipDurability(18, 2.0, 4)
+               ShipWeightClass.DESTROYER -> ShipDurability(8, 0.5, 1)
+               ShipWeightClass.CRUISER -> ShipDurability(12, 1.0, 2)
+               ShipWeightClass.BATTLECRUISER -> ShipDurability(14, 1.5, 2)
+               ShipWeightClass.BATTLESHIP -> ShipDurability(16, 2.0, 3)
                
-               ShipWeightClass.GRAND_CRUISER -> ShipDurability(16, 1.5, 3)
-               ShipWeightClass.COLOSSUS -> ShipDurability(27, 3.0, 5)
+               ShipWeightClass.GRAND_CRUISER -> ShipDurability(15, 1.75, 3)
+               ShipWeightClass.COLOSSUS -> ShipDurability(27, 3.0, 4)
                
                ShipWeightClass.AUXILIARY_SHIP -> ShipDurability(4, 2.0, 1)
                ShipWeightClass.LIGHT_CRUISER -> ShipDurability(8, 3.0, 2)
-               ShipWeightClass.MEDIUM_CRUISER -> ShipDurability(12, 3.5, 3)
-               ShipWeightClass.HEAVY_CRUISER -> ShipDurability(16, 4.0, 4)
+               ShipWeightClass.MEDIUM_CRUISER -> ShipDurability(12, 3.5, 2)
+               ShipWeightClass.HEAVY_CRUISER -> ShipDurability(16, 4.0, 3)
                
                ShipWeightClass.FRIGATE -> ShipDurability(10, 1.5, 1)
                ShipWeightClass.LINE_SHIP -> ShipDurability(15, 2.0, 1)
                ShipWeightClass.DREADNOUGHT -> ShipDurability(20, 2.5, 1)
        }
+
+@Serializable
+data class ShipFirepower(
+       val rangeMultiplier: Double,
+       val criticalChance: Double,
+       val cannonAccuracy: Double,
+       val lanceCharging: Double,
+)
+
+val ShipWeightClass.firepower: ShipFirepower
+       get() = when (this) {
+               ShipWeightClass.ESCORT -> ShipFirepower(0.75, 0.75, 0.875, 0.875)
+               ShipWeightClass.DESTROYER -> ShipFirepower(0.75, 0.75, 1.0, 1.0)
+               ShipWeightClass.CRUISER -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               ShipWeightClass.BATTLECRUISER -> ShipFirepower(1.25, 1.25, 1.25, 1.25)
+               ShipWeightClass.BATTLESHIP -> ShipFirepower(1.25, 1.25, 1.25, 1.25)
+               
+               ShipWeightClass.GRAND_CRUISER -> ShipFirepower(1.25, 1.25, 1.25, 1.25)
+               ShipWeightClass.COLOSSUS -> ShipFirepower(1.5, 1.5, 1.5, 1.5)
+               
+               ShipWeightClass.AUXILIARY_SHIP -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               ShipWeightClass.LIGHT_CRUISER -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               ShipWeightClass.MEDIUM_CRUISER -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               ShipWeightClass.HEAVY_CRUISER -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               
+               ShipWeightClass.FRIGATE -> ShipFirepower(0.8, 0.8, 1.0, 1.0)
+               ShipWeightClass.LINE_SHIP -> ShipFirepower(1.0, 1.0, 1.0, 1.0)
+               ShipWeightClass.DREADNOUGHT -> ShipFirepower(1.2, 1.2, 1.0, 1.0)
+       }
index c2ef0648ab9c44f361b9fb9cf3701d2a341cf1a0..b7a9e7b26392e203e01852dbb32bee45fd1aff4a 100644 (file)
@@ -49,7 +49,7 @@ data class ShipInstance(
                return when (weapon) {
                        is ShipWeaponInstance.Cannon -> weaponAmount > 0
                        is ShipWeaponInstance.Hangar -> weapon.wingHealth > 0.0
-                       is ShipWeaponInstance.Lance -> weapon.numCharges > 0
+                       is ShipWeaponInstance.Lance -> weapon.numCharges > EPSILON
                        is ShipWeaponInstance.Torpedo -> true
                        is ShipWeaponInstance.MegaCannon -> weapon.remainingShots > 0
                        is ShipWeaponInstance.RevelationGun -> weapon.remainingShots > 0
@@ -146,6 +146,9 @@ val ShipInstance.movement: ShipMovement
                }
        }
 
+val ShipInstance.firepower: ShipFirepower
+       get() = ship.firepower
+
 fun Ship.defaultPowerMode(): ShipPowerMode {
        val amount = reactor.subsystemAmount
        return ShipPowerMode(amount, amount, amount)
index 2d03f115b28d87e4213611446e0999e69a8c8b90..acb6308f0ea83fa12cac764132ed17fc6ae0c51e 100644 (file)
@@ -125,18 +125,18 @@ sealed class CritResult {
 }
 
 fun ShipInstance.doCriticalDamage(): CritResult {
-       return when (Random.nextInt(0..4) + Random.nextInt(0..4)) { // Ranges in 0..8, probability density peaks at 4
+       return when (Random.nextInt(0..6) + Random.nextInt(0..6)) { // Ranges in 0..12, probability density peaks at 6
                0 -> {
-                       // Damage 3 weapons
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
+                       // Damage ALL the modules!
+                       val modulesDamaged = modulesStatus.statuses.keys.filter { it !is ShipModule.Weapon }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
                        )
                }
                1 -> {
-                       // Damage 1 weapon
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
+                       // Damage 3 weapons
+                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -151,6 +151,22 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                        )
                }
                3 -> {
+                       // Damage 2 random modules
+                       val modulesDamaged = modulesStatus.statuses.keys.filter { it !is ShipModule.Weapon }.shuffled().take(2)
+                       CritResult.ModulesDisabled(
+                               copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
+                               modulesDamaged.toSet()
+                       )
+               }
+               4 -> {
+                       // Damage 1 weapon
+                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
+                       CritResult.ModulesDisabled(
+                               copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
+                               modulesDamaged.toSet()
+                       )
+               }
+               5 -> {
                        // Damage engines
                        val moduleDamaged = ShipModule.Engines
                        CritResult.ModulesDisabled(
@@ -158,13 +174,19 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                                setOf(moduleDamaged)
                        )
                }
-               4 -> {
+               6 -> {
                        // Fire!
                        CritResult.FireStarted(
                                copy(numFires = numFires + 1)
                        )
                }
-               5 -> {
+               7 -> {
+                       // Two fires!
+                       CritResult.FireStarted(
+                               copy(numFires = numFires + 2)
+                       )
+               }
+               8 -> {
                        // Damage turrets
                        val moduleDamaged = ShipModule.Turrets
                        CritResult.ModulesDisabled(
@@ -172,7 +194,15 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                                setOf(moduleDamaged)
                        )
                }
-               6 -> {
+               9 -> {
+                       // Damage random module
+                       val moduleDamaged = modulesStatus.statuses.keys.filter { it !is ShipModule.Weapon }.random()
+                       CritResult.ModulesDisabled(
+                               copy(modulesStatus = modulesStatus.damage(moduleDamaged)),
+                               setOf(moduleDamaged)
+                       )
+               }
+               10 -> {
                        // Damage shields
                        val moduleDamaged = ShipModule.Shields
                        CritResult.ModulesDisabled(
@@ -183,14 +213,14 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                                setOf(moduleDamaged)
                        )
                }
-               7 -> {
+               11 -> {
                        // Hull breach
-                       val damage = Random.nextInt(0, 2) + Random.nextInt(0, 2)
+                       val damage = Random.nextInt(0..2) + Random.nextInt(1..3)
                        CritResult.fromImpactResult(impact(damage))
                }
-               8 -> {
+               12 -> {
                        // Bulkhead collapse
-                       val damage = Random.nextInt(0, 5) + Random.nextInt(0, 5)
+                       val damage = Random.nextInt(2..4) + Random.nextInt(3..5)
                        CritResult.fromImpactResult(impact(damage))
                }
                else -> CritResult.NoEffect
index 66c4129149135116bc0f038787497177dd165cee..af27ed6f23eacbe80b4209efb7c85fa1ce73b4b2 100644 (file)
@@ -3,6 +3,8 @@ package starshipfights.game
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
 import kotlin.math.expm1
+import kotlin.math.floor
+import kotlin.math.roundToInt
 import kotlin.math.sqrt
 import kotlin.random.Random
 
@@ -72,7 +74,7 @@ sealed class ShipWeapon {
                override val addsPointCost: Int
                        get() = numShots * 10
                
-               override fun instantiate() = ShipWeaponInstance.Lance(this, 10)
+               override fun instantiate() = ShipWeaponInstance.Lance(this, 10.0)
        }
        
        @Serializable
@@ -211,9 +213,9 @@ sealed class ShipWeaponInstance {
        data class Cannon(override val weapon: ShipWeapon.Cannon) : ShipWeaponInstance()
        
        @Serializable
-       data class Lance(override val weapon: ShipWeapon.Lance, val numCharges: Int) : ShipWeaponInstance() {
+       data class Lance(override val weapon: ShipWeapon.Lance, val numCharges: Double) : ShipWeaponInstance() {
                val charge: Double
-                       get() = -expm1(-numCharges.toDouble())
+                       get() = -expm1(-numCharges)
        }
        
        @Serializable
@@ -257,7 +259,7 @@ data class ShipInstanceArmaments(
 
 fun cannonChanceToHit(attacker: ShipInstance, targeted: ShipInstance): Double {
        val relativeDistance = attacker.position.location - targeted.position.location
-       return sqrt(SHIP_BASE_SIZE / relativeDistance.length)
+       return sqrt(SHIP_BASE_SIZE / relativeDistance.length) * attacker.firepower.cannonAccuracy
 }
 
 sealed class ImpactResult {
@@ -283,7 +285,7 @@ fun ShipInstance.afterUsing(weaponId: Id<ShipWeapon>) = when (val weapon = armam
        }
        is ShipWeaponInstance.Lance -> {
                val newWeapons = armaments.weaponInstances + mapOf(
-                       weaponId to weapon.copy(numCharges = 0)
+                       weaponId to weapon.copy(numCharges = 0.0)
                )
                
                copy(armaments = ShipInstanceArmaments(newWeapons), usedArmaments = usedArmaments + setOf(weaponId))
@@ -367,6 +369,46 @@ fun ShipInstance.afterTargeted(by: ShipInstance, weaponId: Id<ShipWeapon>) = whe
        }
 }
 
+fun ShipInstance.afterBombed(otherShips: Map<Id<ShipInstance>, ShipInstance>, strikeWingDamage: MutableMap<ShipHangarWing, Double>): ImpactResult {
+       if (bomberWings.isEmpty())
+               return ImpactResult.Damaged(this, null)
+       
+       val totalFighterHealth = fighterWings.sumOf { (carrierId, wingId) ->
+               (otherShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+       } + (if (canUseTurrets) ship.durability.turretDefense else 0.0)
+       
+       val totalBomberHealth = bomberWings.sumOf { (carrierId, wingId) ->
+               (otherShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+       }
+       
+       if (totalBomberHealth < EPSILON)
+               return ImpactResult.Damaged(this, null)
+       
+       val maxBomberWingOutput = smoothNegative(totalBomberHealth - totalFighterHealth)
+       val maxFighterWingOutput = smoothNegative(totalFighterHealth - totalBomberHealth)
+       
+       fighterWings.forEach { strikeWingDamage[it] = Random.nextDouble() * maxBomberWingOutput }
+       bomberWings.forEach { strikeWingDamage[it] = Random.nextDouble() * maxFighterWingOutput }
+       
+       val chanceOfShipDamage = smoothNegative(maxBomberWingOutput - maxFighterWingOutput)
+       val hits = floor(chanceOfShipDamage).let { floored ->
+               floored.roundToInt() + (if (Random.nextDouble() < chanceOfShipDamage - floored) 1 else 0)
+       }
+       
+       val criticalChance = smoothMinus1To1(chanceOfShipDamage, exponent = 0.5)
+       return impact(hits).applyStrikeCraftCriticals(criticalChance)
+}
+
+fun ShipInstance.afterBombing(strikeWingDamage: Map<ShipHangarWing, Double>): ShipInstance {
+       val newArmaments = armaments.weaponInstances.mapValues { (weaponId, weapon) ->
+               if (weapon is ShipWeaponInstance.Hangar)
+                       weapon.copy(wingHealth = weapon.wingHealth - (strikeWingDamage[ShipHangarWing(id, weaponId)] ?: 0.0))
+               else weapon
+       }.filterValues { it !is ShipWeaponInstance.Hangar || it.wingHealth > 0.0 }
+       
+       return copy(armaments = ShipInstanceArmaments(newArmaments))
+}
+
 fun ImpactResult.Damaged.withCritResult(critical: CritResult): ImpactResult = when (critical) {
        is CritResult.NoEffect -> this
        is CritResult.FireStarted -> copy(
@@ -400,19 +442,31 @@ fun ImpactResult.applyCriticals(attacker: ShipInstance, weaponId: Id<ShipWeapon>
        }
 }
 
+fun ImpactResult.applyStrikeCraftCriticals(criticalChance: Double): ImpactResult {
+       return when (this) {
+               is ImpactResult.Destroyed -> this
+               is ImpactResult.Damaged -> {
+                       if (Random.nextDouble() > criticalChance)
+                               this
+                       else
+                               withCritResult(ship.doCriticalDamage())
+               }
+       }
+}
+
 fun criticalChance(attacker: ShipInstance, weaponId: Id<ShipWeapon>, targeted: ShipInstance): Double {
        val targetHasShields = targeted.canUseShields && targeted.shieldAmount > 0
        val weapon = attacker.armaments.weaponInstances[weaponId] ?: return 0.0
        
        return when (weapon) {
                is ShipWeaponInstance.Torpedo -> if (targetHasShields) 0.0 else 0.375
-               is ShipWeaponInstance.Hangar -> 0.0
+               is ShipWeaponInstance.Hangar -> 0.0 // implemented elsewhere
                is ShipWeaponInstance.MegaCannon -> 0.5
                else -> if (targetHasShields) 0.125 else 0.25
-       }
+       } * attacker.firepower.criticalChance
 }
 
-fun getWeaponPickRequest(weapon: ShipWeapon, position: ShipPosition, side: GlobalSide): PickRequest = when (weapon) {
+fun ShipInstance.getWeaponPickRequest(weapon: ShipWeapon, position: ShipPosition, side: GlobalSide): PickRequest = when (weapon) {
        is AreaWeapon -> PickRequest(
                type = PickType.Location(
                        excludesNearShips = emptySet(),
@@ -439,13 +493,19 @@ fun getWeaponPickRequest(weapon: ShipWeapon, position: ShipPosition, side: Globa
                else
                        setOf(side.other)
                
+               val weaponRangeMult = when (weapon) {
+                       is ShipWeapon.Cannon -> firepower.rangeMultiplier
+                       is ShipWeapon.Lance -> firepower.rangeMultiplier
+                       else -> 1.0
+               }
+               
                PickRequest(
                        PickType.Ship(targetSet),
                        PickBoundary.WeaponsFire(
                                center = position.location,
                                facing = position.facing,
                                minDistance = weapon.minRange,
-                               maxDistance = weapon.maxRange,
+                               maxDistance = weapon.maxRange * weaponRangeMult,
                                firingArcs = weapon.firingArcs,
                                canSelfSelect = side in targetSet
                        )
@@ -522,11 +582,6 @@ fun GameState.useWeaponPickResponse(attacker: ShipInstance, weaponId: Id<ShipWea
                        
                        val newChatMessages = chatBox + listOfNotNull(
                                when (impact) {
-                                       is ImpactResult.Destroyed -> ChatEntry.ShipDestroyed(
-                                               impact.ship.id,
-                                               Moment.now,
-                                               ShipAttacker.EnemyShip(newAttacker.id)
-                                       )
                                        is ImpactResult.Damaged -> impact.amount?.let { damage ->
                                                ChatEntry.ShipAttacked(
                                                        impact.ship.id,
@@ -537,6 +592,11 @@ fun GameState.useWeaponPickResponse(attacker: ShipInstance, weaponId: Id<ShipWea
                                                        impact.critical.report(),
                                                )
                                        }
+                                       is ImpactResult.Destroyed -> ChatEntry.ShipDestroyed(
+                                               impact.ship.id,
+                                               Moment.now,
+                                               ShipAttacker.EnemyShip(newAttacker.id)
+                                       )
                                }
                        )
                        
index cfd38dd391d1d0f94592f96c29a9e93d889907ad..94e47fe4368b58e7d3cf938d4ffcff4bc03b37ad 100644 (file)
@@ -5,7 +5,9 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.isActive
 import kotlinx.serialization.json.Json
+import kotlin.math.abs
 import kotlin.math.exp
+import kotlin.math.pow
 import kotlin.math.roundToInt
 
 val jsonSerializer = Json {
@@ -40,6 +42,7 @@ else this + (this * (multiplier - 1))
 
 fun Double.toPercent() = "${(this * 100).roundToInt()}%"
 
+fun smoothMinus1To1(x: Double, exponent: Double = 1.0) = x / (1 + abs(x).pow(exponent)).pow(1 / exponent)
 fun smoothNegative(x: Double) = if (x < 0) exp(x) else x + 1
 
 fun <T> Iterable<T>.joinToDisplayString(oxfordComma: Boolean = true, transform: (T) -> String = { it.toString() }): String = when (val size = count()) {
index ac17564d424e806d5e0af3f76db03c1be3c15cd8..a2bf0f936cb6d29a30822ce254773cb42114303a 100644 (file)
@@ -72,7 +72,6 @@ object GameUI {
                                id = "top-middle-info"
                                
                                p {
-                                       style = "text-align:center;margin:0"
                                        +"Battle has not started yet"
                                }
                        }
@@ -319,8 +318,6 @@ object GameUI {
                topMiddleInfo.clear()
                topMiddleInfo.append {
                        p {
-                               style = "text-align:center;margin:0"
-                               
                                when (state.phase) {
                                        GamePhase.Deploy -> {
                                                strong(classes = "heading") {
@@ -430,12 +427,12 @@ object GameUI {
                                                tr {
                                                        repeat(activeShield) {
                                                                td {
-                                                                       style = "background-color:#69F;margin:20px;height:15px"
+                                                                       style = "background-color:#69F;height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                }
                                                        }
                                                        repeat(downShield) {
                                                                td {
-                                                                       style = "background-color:#46A;margin:20px;height:15px"
+                                                                       style = "background-color:#46A;height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                }
                                                        }
                                                }
@@ -451,12 +448,12 @@ object GameUI {
                                                tr {
                                                        repeat(activeHull) {
                                                                td {
-                                                                       style = "background-color:${if (ship.owner == mySide) "#5F5" else "#F55"};margin:20px;height:15px"
+                                                                       style = "background-color:${if (ship.owner == mySide) "#5F5" else "#F55"};height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                }
                                                        }
                                                        repeat(downHull) {
                                                                td {
-                                                                       style = "background-color:${if (ship.owner == mySide) "#262" else "#622"};margin:20px;height:15px"
+                                                                       style = "background-color:${if (ship.owner == mySide) "#262" else "#622"};height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                }
                                                        }
                                                }
@@ -473,12 +470,12 @@ object GameUI {
                                                        tr {
                                                                repeat(activeWeapons) {
                                                                        td {
-                                                                               style = "background-color:#F63;margin:20px;height:15px"
+                                                                               style = "background-color:#F63;height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                        }
                                                                }
                                                                repeat(downWeapons) {
                                                                        td {
-                                                                               style = "background-color:#A42;margin:20px;height:15px"
+                                                                               style = "background-color:#A42;height:15px;box-shadow:inset 0 0 0 3px #555"
                                                                        }
                                                                }
                                                        }
@@ -632,8 +629,6 @@ object GameUI {
                                        style = "text-align:center;margin:0"
                                        
                                        +"No ship selected. Click on a ship to select it."
-                                       
-                                       hr { style = "border-color:#555" }
                                }
                        } else {
                                val shipAbilities = abilities
@@ -692,171 +687,173 @@ object GameUI {
                                        if (ship.numFires > 0)
                                                span {
                                                        style = "color:#e94"
-                                                       +"${ship.numFires} Onboard Fires"
+                                                       +"${ship.numFires} Onboard Fire${if (ship.numFires == 1) "" else "s"}"
                                                }
                                }
                                
-                               hr { style = "border-color:#555" }
-                               
-                               p {
-                                       style = "height:69%;margin:0"
-                                       
-                                       if (gameState.phase is GamePhase.Repair) {
-                                               +"${ship.remainingRepairTokens} Repair Tokens"
-                                               br
-                                       }
+                               if (ship.owner == mySide) {
+                                       hr { style = "border-color:#555" }
                                        
-                                       shipAbilities.forEach { ability ->
-                                               when (ability) {
-                                                       is PlayerAbilityType.DistributePower -> {
-                                                               val shipPowerMode = ClientAbilityData.newShipPowerModes[ship.id] ?: ship.powerMode
-                                                               
-                                                               table {
-                                                                       style = "width:100%;table-layout:fixed;background-color:#555"
-                                                                       tr {
-                                                                               ShipSubsystem.values().forEach { subsystem ->
-                                                                                       val amount = shipPowerMode[subsystem]
-                                                                                       
-                                                                                       repeat(amount) {
-                                                                                               td {
-                                                                                                       style = "background-color:${subsystem.htmlColor};margin:1px;height:0.55em"
+                                       p {
+                                               style = "height:69%;margin:0"
+                                               
+                                               if (gameState.phase is GamePhase.Repair) {
+                                                       +"${ship.remainingRepairTokens} Repair Tokens"
+                                                       br
+                                               }
+                                               
+                                               shipAbilities.forEach { ability ->
+                                                       when (ability) {
+                                                               is PlayerAbilityType.DistributePower -> {
+                                                                       val shipPowerMode = ClientAbilityData.newShipPowerModes[ship.id] ?: ship.powerMode
+                                                                       
+                                                                       table {
+                                                                               style = "width:100%;table-layout:fixed;background-color:#555"
+                                                                               tr {
+                                                                                       ShipSubsystem.values().forEach { subsystem ->
+                                                                                               val amount = shipPowerMode[subsystem]
+                                                                                               
+                                                                                               repeat(amount) {
+                                                                                                       td {
+                                                                                                               style = "background-color:${subsystem.htmlColor};margin:1px;height:0.55em"
+                                                                                                       }
                                                                                                }
                                                                                        }
                                                                                }
                                                                        }
-                                                               }
-                                                               
-                                                               p {
-                                                                       style = "text-align:center"
-                                                                       +"Power Output: ${ship.ship.reactor.powerOutput}"
-                                                                       br
-                                                                       +"Remaining Transfers: ${ship.remainingGridEfficiency(shipPowerMode)}"
-                                                               }
-                                                               
-                                                               ShipSubsystem.values().forEach { transferFrom ->
-                                                                       div(classes = "button-set row") {
-                                                                               ShipSubsystem.values().filter { it != transferFrom }.forEach { transferTo ->
-                                                                                       button {
-                                                                                               style = "font-size:0.8em;padding:0 0.25em"
-                                                                                               title = "${transferFrom.displayName} to ${transferTo.displayName}"
-                                                                                               
-                                                                                               img(src = transferFrom.imageUrl, alt = transferFrom.displayName) {
-                                                                                                       style = "width:0.95em"
-                                                                                               }
-                                                                                               +Entities.nbsp
-                                                                                               img(src = ShipSubsystem.transferImageUrl, alt = " to ") {
-                                                                                                       style = "width:0.95em"
-                                                                                               }
-                                                                                               +Entities.nbsp
-                                                                                               img(src = transferTo.imageUrl, alt = transferTo.displayName) {
-                                                                                                       style = "width:0.95em"
-                                                                                               }
-                                                                                               
-                                                                                               val delta = mapOf(transferFrom to -1, transferTo to 1)
-                                                                                               val newPowerMode = shipPowerMode + delta
-                                                                                               
-                                                                                               if (ship.validatePowerMode(newPowerMode))
-                                                                                                       onClickFunction = { e ->
-                                                                                                               e.preventDefault()
-                                                                                                               ClientAbilityData.newShipPowerModes[ship.id] = newPowerMode
-                                                                                                               updateAbilityData(gameState)
+                                                                       
+                                                                       p {
+                                                                               style = "text-align:center"
+                                                                               +"Power Output: ${ship.ship.reactor.powerOutput}"
+                                                                               br
+                                                                               +"Remaining Transfers: ${ship.remainingGridEfficiency(shipPowerMode)}"
+                                                                       }
+                                                                       
+                                                                       ShipSubsystem.values().forEach { transferFrom ->
+                                                                               div(classes = "button-set row") {
+                                                                                       ShipSubsystem.values().filter { it != transferFrom }.forEach { transferTo ->
+                                                                                               button {
+                                                                                                       style = "font-size:0.8em;padding:0 0.25em"
+                                                                                                       title = "${transferFrom.displayName} to ${transferTo.displayName}"
+                                                                                                       
+                                                                                                       img(src = transferFrom.imageUrl, alt = transferFrom.displayName) {
+                                                                                                               style = "width:0.95em"
+                                                                                                       }
+                                                                                                       +Entities.nbsp
+                                                                                                       img(src = ShipSubsystem.transferImageUrl, alt = " to ") {
+                                                                                                               style = "width:0.95em"
+                                                                                                       }
+                                                                                                       +Entities.nbsp
+                                                                                                       img(src = transferTo.imageUrl, alt = transferTo.displayName) {
+                                                                                                               style = "width:0.95em"
+                                                                                                       }
+                                                                                                       
+                                                                                                       val delta = mapOf(transferFrom to -1, transferTo to 1)
+                                                                                                       val newPowerMode = shipPowerMode + delta
+                                                                                                       
+                                                                                                       if (ship.validatePowerMode(newPowerMode))
+                                                                                                               onClickFunction = { e ->
+                                                                                                                       e.preventDefault()
+                                                                                                                       ClientAbilityData.newShipPowerModes[ship.id] = newPowerMode
+                                                                                                                       updateAbilityData(gameState)
+                                                                                                               }
+                                                                                                       else {
+                                                                                                               disabled = true
+                                                                                                               style += ";cursor:not-allowed"
                                                                                                        }
-                                                                                               else {
-                                                                                                       disabled = true
-                                                                                                       style += ";cursor:not-allowed"
                                                                                                }
                                                                                        }
                                                                                }
                                                                        }
-                                                               }
-                                                               
-                                                               button {
-                                                                       +"Confirm"
-                                                                       if (ship.validatePowerMode(shipPowerMode))
+                                                                       
+                                                                       button {
+                                                                               +"Confirm"
+                                                                               if (ship.validatePowerMode(shipPowerMode))
+                                                                                       onClickFunction = { e ->
+                                                                                               e.preventDefault()
+                                                                                               responder.useAbility(ability)
+                                                                                       }
+                                                                               else {
+                                                                                       disabled = true
+                                                                                       style = "cursor:not-allowed"
+                                                                               }
+                                                                       }
+                                                                       
+                                                                       button {
+                                                                               +"Reset"
                                                                                onClickFunction = { e ->
                                                                                        e.preventDefault()
-                                                                                       responder.useAbility(ability)
+                                                                                       ClientAbilityData.newShipPowerModes[ship.id] = ship.powerMode
+                                                                                       updateAbilityData(gameState)
                                                                                }
-                                                                       else {
-                                                                               disabled = true
-                                                                               style = "cursor:not-allowed"
                                                                        }
                                                                }
-                                                               
-                                                               button {
-                                                                       +"Reset"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               ClientAbilityData.newShipPowerModes[ship.id] = ship.powerMode
-                                                                               updateAbilityData(gameState)
-                                                                       }
-                                                               }
-                                                       }
-                                                       is PlayerAbilityType.MoveShip -> {
-                                                               button {
-                                                                       +"Move Ship"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                                               is PlayerAbilityType.MoveShip -> {
+                                                                       button {
+                                                                               +"Move Ship"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
                                                                }
-                                                       }
-                                                       is PlayerAbilityType.RepairShipModule -> {
-                                                               a(href = "#") {
-                                                                       +"Repair ${ability.module.getDisplayName(ship.ship)}"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                                               is PlayerAbilityType.RepairShipModule -> {
+                                                                       a(href = "#") {
+                                                                               +"Repair ${ability.module.getDisplayName(ship.ship)}"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
+                                                                       br
                                                                }
-                                                               br
-                                                       }
-                                                       is PlayerAbilityType.ExtinguishFire -> {
-                                                               a(href = "#") {
-                                                                       +"Extinguish Fire"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                                               is PlayerAbilityType.ExtinguishFire -> {
+                                                                       a(href = "#") {
+                                                                               +"Extinguish Fire"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
+                                                                       br
                                                                }
-                                                               br
                                                        }
                                                }
-                                       }
-                                       
-                                       combatAbilities.forEach { ability ->
-                                               br
-                                               
-                                               val weaponInstance = ship.armaments.weaponInstances.getValue(ability.weapon)
                                                
-                                               val weaponVerb = if (weaponInstance is ShipWeaponInstance.Hangar) "Release" else "Fire"
-                                               val weaponDesc = weaponInstance.displayName
-                                               
-                                               when (ability) {
-                                                       is PlayerAbilityType.ChargeLance -> {
-                                                               a(href = "#") {
-                                                                       +"Charge $weaponDesc"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                               combatAbilities.forEach { ability ->
+                                                       br
+                                                       
+                                                       val weaponInstance = ship.armaments.weaponInstances.getValue(ability.weapon)
+                                                       
+                                                       val weaponVerb = if (weaponInstance is ShipWeaponInstance.Hangar) "Release" else "Fire"
+                                                       val weaponDesc = weaponInstance.displayName
+                                                       
+                                                       when (ability) {
+                                                               is PlayerAbilityType.ChargeLance -> {
+                                                                       a(href = "#") {
+                                                                               +"Charge $weaponDesc"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
                                                                }
-                                                       }
-                                                       is PlayerAbilityType.UseWeapon -> {
-                                                               a(href = "#") {
-                                                                       +"$weaponVerb $weaponDesc"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                                               is PlayerAbilityType.UseWeapon -> {
+                                                                       a(href = "#") {
+                                                                               +"$weaponVerb $weaponDesc"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
                                                                }
-                                                       }
-                                                       is PlayerAbilityType.RecallStrikeCraft -> {
-                                                               a(href = "#") {
-                                                                       +"Recall $weaponDesc"
-                                                                       onClickFunction = { e ->
-                                                                               e.preventDefault()
-                                                                               responder.useAbility(ability)
+                                                               is PlayerAbilityType.RecallStrikeCraft -> {
+                                                                       a(href = "#") {
+                                                                               +"Recall $weaponDesc"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
                                                                        }
                                                                }
                                                        }
index 0b41b752ca9eec9a71ca7c685c0afbef524c51a6..b386b9363926fe6431ae8d4a7a452b7063f57d6d 100644 (file)
@@ -196,6 +196,16 @@ div.ui-layer {
        padding-top: 0.75em;
 }
 
+#top-middle-info p {
+       text-align: center;
+       margin: 0;
+
+       position: absolute;
+       top: 50%;
+       left: 50%;
+       transform: translate(-50%, -50%);
+}
+
 #top-right-bar {
        position: fixed;
        top: 2.5vh;
index 606027b3d2ce7c0ca0bce8f5330a281e0de77888..a8bcfcdf7890766318f756c10649e59cc14dc268 100644 (file)
@@ -136,7 +136,7 @@ interface AuthProvider {
                                                name = form["name"]?.takeIf { it.isNotBlank() && it.length <= ADMIRAL_NAME_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("That name is not valid - must not be blank, must not be longer than $ADMIRAL_NAME_MAX_LENGTH characters")),
                                                isFemale = form.getOrFail("sex") == "female",
                                                faction = Faction.valueOf(form.getOrFail("faction")),
-                                               acumen = 0,
+                                               acumen = if (CurrentConfiguration.isDevEnv) 20_000 else 0,
                                                money = 500
                                        )
                                        val newShips = generateFleet(newAdmiral)
index ac207158cceccd34130eac17879d0b25cf70fb64..18164254e7a6e5ca84150608e868effce8da9c67 100644 (file)
@@ -2,10 +2,9 @@ package starshipfights.data.admiralty
 
 import starshipfights.game.Faction
 import starshipfights.game.ShipWeightClass
-import kotlin.random.Random
 
-fun newShipName(faction: Faction, shipWeightClass: ShipWeightClass, existingNames: MutableSet<String>, random: Random = Random) = generateSequence {
-       ShipNames.nameShip(faction, shipWeightClass, random)
+fun newShipName(faction: Faction, shipWeightClass: ShipWeightClass, existingNames: MutableSet<String>) = generateSequence {
+       ShipNames.nameShip(faction, shipWeightClass)
 }.take(20).dropWhile { it in existingNames }.firstOrNull()?.also { existingNames.add(it) }
 
 object ShipNames {
@@ -121,12 +120,12 @@ object ShipNames {
                "King Kaleb of Axum"
        )
        
-       private fun nameMechyrdianShip(weightClass: ShipWeightClass, randomChooser: Random) = when (weightClass) {
-               ShipWeightClass.ESCORT -> "${mechyrdianFrigateNames1.random(randomChooser)} ${mechyrdianFrigateNames2.random(randomChooser)}"
-               ShipWeightClass.DESTROYER -> "${mechyrdianFrigateNames1.random(randomChooser)} ${mechyrdianFrigateNames2.random(randomChooser)}"
-               ShipWeightClass.CRUISER -> "${mechyrdianCruiserNames1.random(randomChooser)} ${mechyrdianCruiserNames2.random(randomChooser)}"
-               ShipWeightClass.BATTLECRUISER -> "${mechyrdianCruiserNames1.random(randomChooser)} ${mechyrdianCruiserNames2.random(randomChooser)}"
-               ShipWeightClass.BATTLESHIP -> mechyrdianBattleshipNames.random(randomChooser)
+       private fun nameMechyrdianShip(weightClass: ShipWeightClass) = when (weightClass) {
+               ShipWeightClass.ESCORT -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
+               ShipWeightClass.DESTROYER -> "${mechyrdianFrigateNames1.random()} ${mechyrdianFrigateNames2.random()}"
+               ShipWeightClass.CRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
+               ShipWeightClass.BATTLECRUISER -> "${mechyrdianCruiserNames1.random()} ${mechyrdianCruiserNames2.random()}"
+               ShipWeightClass.BATTLESHIP -> mechyrdianBattleshipNames.random()
                else -> error("Invalid Mechyrdian ship weight!")
        }
        
@@ -183,9 +182,9 @@ object ShipNames {
        
        private const val masraDraetsenColossusName = "Boukephalas"
        
-       private fun nameMasraDraetsenShip(weightClass: ShipWeightClass, randomChooser: Random) = if (weightClass == ShipWeightClass.COLOSSUS)
+       private fun nameMasraDraetsenShip(weightClass: ShipWeightClass) = if (weightClass == ShipWeightClass.COLOSSUS)
                masraDraetsenColossusName
-       else "${masraDraetsenShipNames1.random(randomChooser)} ${masraDraetsenShipNames2.random(randomChooser)}"
+       else "${masraDraetsenShipNames1.random()} ${masraDraetsenShipNames2.random()}"
        
        private val isarnareykkShipNames = listOf(
                "Professional with Standards",
@@ -217,7 +216,7 @@ object ShipNames {
                "Praethoris Khorr Gaming",
        )
        
-       private fun nameIsarnareykskShip(randomChooser: Random) = isarnareykkShipNames.random(randomChooser)
+       private fun nameIsarnareykskShip() = isarnareykkShipNames.random()
        
        private val vestigiumShipNames = listOf(
                // NAMED AFTER SPACE SHUTTLES
@@ -305,12 +304,12 @@ object ShipNames {
                "Ilya Korochenko"
        )
        
-       private fun nameAmericanShip(randomChooser: Random) = vestigiumShipNames.random(randomChooser)
+       private fun nameAmericanShip() = vestigiumShipNames.random()
        
-       fun nameShip(faction: Faction, weightClass: ShipWeightClass, randomChooser: Random = Random): String = when (faction) {
-               Faction.MECHYRDIA -> nameMechyrdianShip(weightClass, randomChooser)
-               Faction.MASRA_DRAETSEN -> nameMasraDraetsenShip(weightClass, randomChooser)
-               Faction.ISARNAREYKK -> nameIsarnareykskShip(randomChooser)
-               Faction.VESTIGIUM -> nameAmericanShip(randomChooser)
+       fun nameShip(faction: Faction, weightClass: ShipWeightClass): String = when (faction) {
+               Faction.MECHYRDIA -> nameMechyrdianShip(weightClass)
+               Faction.MASRA_DRAETSEN -> nameMasraDraetsenShip(weightClass)
+               Faction.ISARNAREYKK -> nameIsarnareykskShip()
+               Faction.VESTIGIUM -> nameAmericanShip()
        }
 }
index 8002e58af45beec45d3b1707ede15f525cc1a159..60fefd54d62a258111f6cd4806c8a6988cfe92f0 100644 (file)
@@ -20,7 +20,6 @@ import org.litote.kmongo.serialization.registerSerializer
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import java.io.File
-import java.net.ServerSocket
 import kotlin.system.exitProcess
 
 @Serializable
@@ -30,8 +29,6 @@ sealed class ConnectionType {
        @Serializable
        @SerialName("embedded")
        data class Embedded(val dataDir: String = "mongodb") : ConnectionType() {
-               private fun getFreePort() = ServerSocket(0).use { it.localPort }
-               
                @Transient
                val log: Logger = LoggerFactory.getLogger(javaClass)
                
@@ -40,12 +37,11 @@ sealed class ConnectionType {
                        
                        val starter = MongodStarter.getDefaultInstance()
                        
-                       val port = getFreePort()
-                       log.info("Running embedded MongoDB on port $port")
+                       log.info("Running embedded MongoDB on port 27017")
                        
                        val config = MongodConfig.builder()
                                .version(Version.Main.PRODUCTION)
-                               .net(Net(port, Network.localhostIsIPv6()))
+                               .net(Net(27017, Network.localhostIsIPv6()))
                                .replication(Storage(dataDirPath, null, 1024))
                                .cmdOptions(MongoCmdOptions.builder().useNoJournal(false).build())
                                .build()
@@ -66,7 +62,7 @@ sealed class ConnectionType {
                                exitProcess(-1)
                        }
                        
-                       return "mongodb://localhost:$port"
+                       return "mongodb://localhost:27017"
                }
        }
        
index 6a3ec9981fe41696e53d37599efecb9695d41d32..21668624d16e2a0d924c39b88718d92a4f0b7ae3 100644 (file)
@@ -2,11 +2,10 @@ package starshipfights.game
 
 import starshipfights.data.admiralty.getAdmiralsShips
 import kotlin.math.PI
-import kotlin.random.Random
 
-suspend fun generateGameStart(hostInfo: InGameAdmiral, guestInfo: InGameAdmiral, battleInfo: BattleInfo, random: Random = Random): GameStart {
-       val battleWidth = (25..35).random(random) * 500.0
-       val battleLength = (15..45).random(random) * 500.0
+suspend fun generateGameStart(hostInfo: InGameAdmiral, guestInfo: InGameAdmiral, battleInfo: BattleInfo): GameStart {
+       val battleWidth = (25..35).random() * 500.0
+       val battleLength = (15..45).random() * 500.0
        
        val deployWidth2 = battleWidth / 2
        val deployLength2 = 875.0
index 5b40fc7b3ab981cd3542f907193e04a0381afb1a..788b9e6e0a108718a63b64d3993a6c780c8938ea 100644 (file)
@@ -120,6 +120,22 @@ suspend fun ApplicationCall.shipPage(shipType: ShipType): HTML.() -> Unit = page
                                        +shipType.weightClass.reactor.gridEfficiency.toString()
                                }
                        }
+                       tr {
+                               th { +"Base Crit Chance" }
+                               th { +"Cannon Targeting" }
+                               th { +"Lance Efficiency" }
+                       }
+                       tr {
+                               td {
+                                       +shipType.weightClass.firepower.criticalChance.toPercent()
+                               }
+                               td {
+                                       +shipType.weightClass.firepower.cannonAccuracy.toPercent()
+                               }
+                               td {
+                                       +shipType.weightClass.firepower.lanceCharging.toPercent()
+                               }
+                       }
                }
                table {
                        tr {
@@ -146,8 +162,14 @@ suspend fun ApplicationCall.shipPage(shipType: ShipType): HTML.() -> Unit = page
                                                }
                                        }
                                        td {
+                                               val weaponRangeMult = when (weapon) {
+                                                       is ShipWeapon.Cannon -> shipType.weightClass.firepower.rangeMultiplier
+                                                       is ShipWeapon.Lance -> shipType.weightClass.firepower.rangeMultiplier
+                                                       else -> 1.0
+                                               }
+                                               
                                                weapon.minRange.takeIf { it != SHIP_BASE_SIZE }?.let { +"${it.roundToInt()}-" }
-                                               +"${weapon.maxRange.roundToInt()} meters"
+                                               +"${(weapon.maxRange * weaponRangeMult).roundToInt()} meters"
                                                if (weapon is AreaWeapon) {
                                                        br
                                                        +"${weapon.areaRadius.roundToInt()} meter impact radius"