}
companion object {
- fun fromAcumen(acumen: Int) = values().firstOrNull { it.minAcumen <= acumen } ?: REAR_ADMIRAL
+ fun fromAcumen(acumen: Int) = values().lastOrNull { it.minAcumen <= acumen } ?: values().first()
}
}
import kotlinx.serialization.Serializable
import starshipfights.data.Id
+import kotlin.math.abs
sealed interface ShipAbility {
val ship: Id<ShipInstance>
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,
+ )
)
}
}
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)
)
)
)
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) }
}
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")
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
}
}
import kotlinx.serialization.Serializable
import starshipfights.data.Id
-import kotlin.math.abs
import kotlin.random.Random
import kotlin.random.nextInt
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
}
}.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)
}
}.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(),
)
}
}
val durability: ShipDurability
get() = shipType.weightClass.durability
+ val firepower: ShipFirepower
+ get() = shipType.weightClass.firepower
+
val armaments: ShipArmaments
get() = shipType.armaments
}
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)
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)
+ }
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
}
}
+val ShipInstance.firepower: ShipFirepower
+ get() = ship.firepower
+
fun Ship.defaultPowerMode(): ShipPowerMode {
val amount = reactor.subsystemAmount
return ShipPowerMode(amount, amount, amount)
}
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()
)
}
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(
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(
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(
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
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
override val addsPointCost: Int
get() = numShots * 10
- override fun instantiate() = ShipWeaponInstance.Lance(this, 10)
+ override fun instantiate() = ShipWeaponInstance.Lance(this, 10.0)
}
@Serializable
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
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 {
}
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))
}
}
+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(
}
}
+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(),
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
)
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,
impact.critical.report(),
)
}
+ is ImpactResult.Destroyed -> ChatEntry.ShipDestroyed(
+ impact.ship.id,
+ Moment.now,
+ ShipAttacker.EnemyShip(newAttacker.id)
+ )
}
)
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 {
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()) {
id = "top-middle-info"
p {
- style = "text-align:center;margin:0"
+"Battle has not started yet"
}
}
topMiddleInfo.clear()
topMiddleInfo.append {
p {
- style = "text-align:center;margin:0"
-
when (state.phase) {
GamePhase.Deploy -> {
strong(classes = "heading") {
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"
}
}
}
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"
}
}
}
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"
}
}
}
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
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)
+ }
}
}
}
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;
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)
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 {
"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!")
}
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",
"Praethoris Khorr Gaming",
)
- private fun nameIsarnareykskShip(randomChooser: Random) = isarnareykkShipNames.random(randomChooser)
+ private fun nameIsarnareykskShip() = isarnareykkShipNames.random()
private val vestigiumShipNames = listOf(
// NAMED AFTER SPACE SHUTTLES
"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()
}
}
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
-import java.net.ServerSocket
import kotlin.system.exitProcess
@Serializable
@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)
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()
exitProcess(-1)
}
- return "mongodb://localhost:$port"
+ return "mongodb://localhost:27017"
}
}
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
+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 {
}
}
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"