From: TheSaminator Date: Sun, 13 Mar 2022 21:50:33 +0000 (-0400) Subject: So many changes and fixes this might as well be called Starship Fights 3 X-Git-Url: https://gitweb.starshipfights.net/?a=commitdiff_plain;h=bb8a67326421ce00e88ad2e38899de0debd8a506;p=starship-fights So many changes and fixes this might as well be called Starship Fights 3 --- diff --git a/src/commonMain/kotlin/starshipfights/game/admiralty.kt b/src/commonMain/kotlin/starshipfights/game/admiralty.kt index acdafea..6376e05 100644 --- a/src/commonMain/kotlin/starshipfights/game/admiralty.kt +++ b/src/commonMain/kotlin/starshipfights/game/admiralty.kt @@ -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() } } diff --git a/src/commonMain/kotlin/starshipfights/game/game_ability.kt b/src/commonMain/kotlin/starshipfights/game/game_ability.kt index 5102bcc..50a791c 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_ability.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_ability.kt @@ -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 @@ -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 } } diff --git a/src/commonMain/kotlin/starshipfights/game/game_state.kt b/src/commonMain/kotlin/starshipfights/game/game_state.kt index 555dfaa..4f75606 100644 --- a/src/commonMain/kotlin/starshipfights/game/game_state.kt +++ b/src/commonMain/kotlin/starshipfights/game/game_state.kt @@ -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() 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() // 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(), ) } } diff --git a/src/commonMain/kotlin/starshipfights/game/ship.kt b/src/commonMain/kotlin/starshipfights/game/ship.kt index e18baa9..60552bd 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship.kt @@ -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) + } diff --git a/src/commonMain/kotlin/starshipfights/game/ship_instances.kt b/src/commonMain/kotlin/starshipfights/game/ship_instances.kt index c2ef064..b7a9e7b 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_instances.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_instances.kt @@ -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) diff --git a/src/commonMain/kotlin/starshipfights/game/ship_modules.kt b/src/commonMain/kotlin/starshipfights/game/ship_modules.kt index 2d03f11..acb6308 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_modules.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_modules.kt @@ -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 diff --git a/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt b/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt index 66c4129..af27ed6 100644 --- a/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt +++ b/src/commonMain/kotlin/starshipfights/game/ship_weapons.kt @@ -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) = 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) = whe } } +fun ShipInstance.afterBombed(otherShips: Map, ShipInstance>, strikeWingDamage: MutableMap): 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): 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 } } +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, 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 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 ChatEntry.ShipDestroyed( + impact.ship.id, + Moment.now, + ShipAttacker.EnemyShip(newAttacker.id) + ) } ) diff --git a/src/commonMain/kotlin/starshipfights/game/util.kt b/src/commonMain/kotlin/starshipfights/game/util.kt index cfd38dd..94e47fe 100644 --- a/src/commonMain/kotlin/starshipfights/game/util.kt +++ b/src/commonMain/kotlin/starshipfights/game/util.kt @@ -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 Iterable.joinToDisplayString(oxfordComma: Boolean = true, transform: (T) -> String = { it.toString() }): String = when (val size = count()) { diff --git a/src/jsMain/kotlin/starshipfights/game/game_ui.kt b/src/jsMain/kotlin/starshipfights/game/game_ui.kt index ac17564..a2bf0f9 100644 --- a/src/jsMain/kotlin/starshipfights/game/game_ui.kt +++ b/src/jsMain/kotlin/starshipfights/game/game_ui.kt @@ -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) + } } } } diff --git a/src/jsMain/resources/style.css b/src/jsMain/resources/style.css index 0b41b75..b386b93 100644 --- a/src/jsMain/resources/style.css +++ b/src/jsMain/resources/style.css @@ -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; diff --git a/src/jvmMain/kotlin/starshipfights/auth/providers.kt b/src/jvmMain/kotlin/starshipfights/auth/providers.kt index 606027b..a8bcfcd 100644 --- a/src/jvmMain/kotlin/starshipfights/auth/providers.kt +++ b/src/jvmMain/kotlin/starshipfights/auth/providers.kt @@ -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) diff --git a/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt b/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt index ac20715..1816425 100644 --- a/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt +++ b/src/jvmMain/kotlin/starshipfights/data/admiralty/ship_names.kt @@ -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, random: Random = Random) = generateSequence { - ShipNames.nameShip(faction, shipWeightClass, random) +fun newShipName(faction: Faction, shipWeightClass: ShipWeightClass, existingNames: MutableSet) = 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() } } diff --git a/src/jvmMain/kotlin/starshipfights/data/data_connection.kt b/src/jvmMain/kotlin/starshipfights/data/data_connection.kt index 8002e58..60fefd5 100644 --- a/src/jvmMain/kotlin/starshipfights/data/data_connection.kt +++ b/src/jvmMain/kotlin/starshipfights/data/data_connection.kt @@ -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" } } diff --git a/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt b/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt index 6a3ec99..2166862 100644 --- a/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt +++ b/src/jvmMain/kotlin/starshipfights/game/game_start_jvm.kt @@ -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 diff --git a/src/jvmMain/kotlin/starshipfights/info/views_ships.kt b/src/jvmMain/kotlin/starshipfights/info/views_ships.kt index 5b40fc7..788b9e6 100644 --- a/src/jvmMain/kotlin/starshipfights/info/views_ships.kt +++ b/src/jvmMain/kotlin/starshipfights/info/views_ships.kt @@ -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"