Add defense turrets to ships and rework strike-craft damage
authorTheSaminator <TheSaminator@users.noreply.github.com>
Fri, 4 Mar 2022 20:29:28 +0000 (15:29 -0500)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Fri, 4 Mar 2022 20:29:28 +0000 (15:29 -0500)
20 files changed:
src/commonMain/kotlin/starshipfights/data/data.kt
src/commonMain/kotlin/starshipfights/game/game_ability.kt
src/commonMain/kotlin/starshipfights/game/game_state.kt
src/commonMain/kotlin/starshipfights/game/pick_bounds.kt
src/commonMain/kotlin/starshipfights/game/ship.kt
src/commonMain/kotlin/starshipfights/game/ship_instances.kt
src/commonMain/kotlin/starshipfights/game/ship_types.kt
src/commonMain/kotlin/starshipfights/game/ship_weapons.kt
src/commonMain/kotlin/starshipfights/game/ship_weapons_formats.kt
src/commonMain/kotlin/starshipfights/game/ship_weapons_list.kt
src/commonMain/kotlin/starshipfights/game/util.kt
src/jsMain/kotlin/starshipfights/game/client.kt
src/jsMain/kotlin/starshipfights/game/client_game.kt
src/jsMain/kotlin/starshipfights/game/client_matchmaking.kt
src/jsMain/kotlin/starshipfights/game/game_ui.kt
src/jsMain/kotlin/starshipfights/game/pick_bounds_js.kt
src/jvmMain/kotlin/starshipfights/game/views_game.kt
src/jvmMain/kotlin/starshipfights/info/views_main.kt
src/jvmMain/kotlin/starshipfights/info/views_ships.kt
src/jvmMain/resources/static/images/external-link.svg

index 867e77a50f6917c6048d4e5bce8d00e428eff3f3..7f67836e75c944f0f901cdf01e31a025027bbe3b 100644 (file)
@@ -10,7 +10,7 @@ import kotlin.jvm.JvmInline
 
 @JvmInline
 @Serializable(with = IdSerializer::class)
-value class Id<T>(val id: String) {
+value class Id<@Suppress("unused") T>(val id: String) {
        override fun toString() = id
        
        fun <U> reinterpret() = Id<U>(id)
index add7a057f7f6aa435a3983ed8dc8ef9e71febde0..4f499ba51af9d168f6c718b16cd196b01d09b06b 100644 (file)
@@ -289,6 +289,40 @@ sealed class PlayerAbilityType {
                        return gameState.useWeaponPickResponse(shipInstance, weapon, pickResponse)
                }
        }
+       
+       @Serializable
+       data class RecallStrikeCraft(override val ship: Id<ShipInstance>, override val weapon: Id<ShipWeapon>) : PlayerAbilityType(), CombatAbility {
+               override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
+                       if (gameState.phase !is GamePhase.Attack) return null
+                       val shipInstance = gameState.ships[ship] ?: return null
+                       if (weapon !in shipInstance.usedArmaments) return null
+                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
+                       if (shipWeapon !is ShipWeaponInstance.Hangar) return null
+                       
+                       return PlayerAbilityData.RecallStrikeCraft
+               }
+               
+               override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
+                       if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only recall strike craft during Phase III")
+                       val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
+                       if (weapon !in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot recall unused strike craft")
+                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
+                       if (shipWeapon !is ShipWeaponInstance.Hangar) return GameEvent.InvalidAction("Cannot recall non-hangar weapons")
+                       
+                       val hangarWing = ShipHangarWing(ship, weapon)
+                       
+                       return GameEvent.StateChange(
+                               gameState.copy(
+                                       ships = gameState.ships.mapValues { (_, targetShip) ->
+                                               targetShip.copy(
+                                                       fighterWings = targetShip.fighterWings - hangarWing,
+                                                       bomberWings = targetShip.bomberWings - hangarWing,
+                                               )
+                                       }
+                               )
+                       )
+               }
+       }
 }
 
 @Serializable
@@ -313,6 +347,9 @@ sealed class PlayerAbilityData {
        
        @Serializable
        data class UseWeapon(val target: PickResponse) : PlayerAbilityData()
+       
+       @Serializable
+       object RecallStrikeCraft : PlayerAbilityData()
 }
 
 fun GameState.getPossibleAbilities(forPlayer: GlobalSide): List<PlayerAbilityType> = if (ready == forPlayer)
@@ -362,10 +399,10 @@ else when (phase) {
                val chargeableLances = ships
                        .filterValues { it.owner == forPlayer && it.weaponAmount > 0 }
                        .flatMap { (id, ship) ->
-                               (ship.armaments.weaponInstances - ship.usedArmaments).mapNotNull { (weaponId, weapon) ->
+                               ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
                                        PlayerAbilityType.ChargeLance(id, weaponId).takeIf {
                                                when (weapon) {
-                                                       is ShipWeaponInstance.Lance -> weapon.charge != 1.0
+                                                       is ShipWeaponInstance.Lance -> weapon.charge != 1.0 && weaponId !in ship.usedArmaments
                                                        else -> false
                                                }
                                        }
@@ -375,16 +412,26 @@ else when (phase) {
                val usableWeapons = ships
                        .filterValues { it.owner == forPlayer }
                        .flatMap { (id, ship) ->
-                               (ship.armaments.weaponInstances - ship.usedArmaments).mapNotNull { (weaponId, weapon) ->
+                               ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
                                        PlayerAbilityType.UseWeapon(id, weaponId).takeIf {
                                                weaponId !in ship.usedArmaments && canWeaponBeUsed(ship, weapon)
                                        }
                                }
                        }
                
+               val recallableStrikeWings = ships
+                       .filterValues { it.owner == forPlayer }
+                       .flatMap { (id, ship) ->
+                               ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
+                                       PlayerAbilityType.RecallStrikeCraft(id, weaponId).takeIf {
+                                               weaponId in ship.usedArmaments && weapon is ShipWeaponInstance.Hangar
+                                       }
+                               }
+                       }
+               
                val finishAttacking = listOf(PlayerAbilityType.DonePhase(GamePhase.Attack(phase.turn)))
                
-               chargeableLances + usableWeapons + finishAttacking
+               chargeableLances + usableWeapons + recallableStrikeWings + finishAttacking
        }
 }
 
index 757ed96c155f6b4eb9839e87af1cc93ce2d1ccea..154abeb9e5ca66d79a7b99256b2e078e9aeaca13 100644 (file)
@@ -3,7 +3,6 @@ package starshipfights.game
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
 import kotlin.math.abs
-import kotlin.math.exp
 import kotlin.random.Random
 
 @Serializable
@@ -74,20 +73,20 @@ fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (ready == playerSide
                                
                                val totalFighterHealth = ship.fighterWings.sumOf { (carrierId, wingId) ->
                                        (newShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
-                               }
+                               } + ship.ship.durability.turretDefense
                                
                                val totalBomberHealth = ship.bomberWings.sumOf { (carrierId, wingId) ->
                                        (newShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
                                }
                                
-                               val maxBomberWingOutput = exp(totalBomberHealth - totalFighterHealth)
-                               val maxFighterWingOutput = exp(totalFighterHealth - totalBomberHealth)
+                               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 = (maxBomberWingOutput - maxFighterWingOutput).coerceAtLeast(0.0) / 2
+                               var chanceOfShipDamage = smoothNegative(maxBomberWingOutput - maxFighterWingOutput)
                                while (chanceOfShipDamage >= 1.0) {
                                        hits++
                                        chanceOfShipDamage -= 1.0
@@ -124,8 +123,8 @@ fun GameState.afterPlayerReady(playerSide: GlobalSide) = if (ready == playerSide
                                        weaponAmount = ship.powerMode.weapons,
                                        shieldAmount = (ship.shieldAmount..ship.powerMode.shields).random(),
                                        
-                                       fighterWings = emptyList(),
-                                       bomberWings = emptyList(),
+                                       fighterWings = emptySet(),
+                                       bomberWings = emptySet(),
                                        usedArmaments = emptySet(),
                                )
                        }
index 1faa61acec67088c5c730da6febac5df7f50413d..4181350242677c2a6e642b89e15de4c73f04993c 100644 (file)
@@ -116,9 +116,14 @@ sealed class PickBoundary {
                val facing: Double,
                val minDistance: Double,
                val maxDistance: Double,
-               val firingArcs: Set<FiringArc>
+               val firingArcs: Set<FiringArc>,
+               
+               val canSelfSelect: Boolean = false
        ) : PickBoundary() {
                override fun contains(point: Position): Boolean {
+                       if (canSelfSelect && (point - center).length < EPSILON)
+                               return true
+                       
                        val r = point - center
                        if (r.length !in minDistance..maxDistance)
                                return false
index 274c33b50945335f6bc27f94dcc800070033f886..c411d92705e884b71b3d598fa7e072c3322e8772 100644 (file)
@@ -50,6 +50,9 @@ val ShipWeightClass.reactor: ShipReactor
                ShipWeightClass.GRAND_CRUISER -> ShipReactor(6, 4)
                ShipWeightClass.COLOSSUS -> ShipReactor(9, 6)
                
+               ShipWeightClass.AUXILIARY_SHIP -> ShipReactor(2, 1)
+               ShipWeightClass.LIGHT_CRUISER -> ShipReactor(3, 1)
+               ShipWeightClass.MEDIUM_CRUISER -> ShipReactor(4, 2)
                ShipWeightClass.HEAVY_CRUISER -> ShipReactor(6, 3)
                
                ShipWeightClass.FRIGATE -> ShipReactor(4, 1)
@@ -74,6 +77,9 @@ val ShipWeightClass.movement: ShipMovement
                ShipWeightClass.GRAND_CRUISER -> ShipMovement(PI / 4, 600.0)
                ShipWeightClass.COLOSSUS -> ShipMovement(PI / 6, 400.0)
                
+               ShipWeightClass.AUXILIARY_SHIP -> ShipMovement(PI / 2, 800.0)
+               ShipWeightClass.LIGHT_CRUISER -> ShipMovement(PI / 2, 700.0)
+               ShipWeightClass.MEDIUM_CRUISER -> ShipMovement(PI / 3, 600.0)
                ShipWeightClass.HEAVY_CRUISER -> ShipMovement(PI / 3, 500.0)
                
                ShipWeightClass.FRIGATE -> ShipMovement(PI * 2 / 3, 1000.0)
@@ -84,22 +90,26 @@ val ShipWeightClass.movement: ShipMovement
 @Serializable
 data class ShipDurability(
        val maxHullPoints: Int,
+       val turretDefense: Double,
 )
 
 val ShipWeightClass.durability: ShipDurability
        get() = when (this) {
-               ShipWeightClass.ESCORT -> ShipDurability(2)
-               ShipWeightClass.DESTROYER -> ShipDurability(4)
-               ShipWeightClass.CRUISER -> ShipDurability(6)
-               ShipWeightClass.BATTLECRUISER -> ShipDurability(7)
-               ShipWeightClass.BATTLESHIP -> ShipDurability(9)
+               ShipWeightClass.ESCORT -> ShipDurability(2, 0.5)
+               ShipWeightClass.DESTROYER -> ShipDurability(4, 0.5)
+               ShipWeightClass.CRUISER -> ShipDurability(6, 1.0)
+               ShipWeightClass.BATTLECRUISER -> ShipDurability(7, 1.0)
+               ShipWeightClass.BATTLESHIP -> ShipDurability(9, 2.0)
                
-               ShipWeightClass.GRAND_CRUISER -> ShipDurability(8)
-               ShipWeightClass.COLOSSUS -> ShipDurability(13)
+               ShipWeightClass.GRAND_CRUISER -> ShipDurability(8, 1.5)
+               ShipWeightClass.COLOSSUS -> ShipDurability(13, 3.0)
                
-               ShipWeightClass.HEAVY_CRUISER -> ShipDurability(8)
+               ShipWeightClass.AUXILIARY_SHIP -> ShipDurability(2, 2.0)
+               ShipWeightClass.LIGHT_CRUISER -> ShipDurability(4, 3.0)
+               ShipWeightClass.MEDIUM_CRUISER -> ShipDurability(6, 3.5)
+               ShipWeightClass.HEAVY_CRUISER -> ShipDurability(8, 4.0)
                
-               ShipWeightClass.FRIGATE -> ShipDurability(4)
-               ShipWeightClass.LINE_SHIP -> ShipDurability(7)
-               ShipWeightClass.DREADNOUGHT -> ShipDurability(10)
+               ShipWeightClass.FRIGATE -> ShipDurability(4, 1.5)
+               ShipWeightClass.LINE_SHIP -> ShipDurability(7, 2.0)
+               ShipWeightClass.DREADNOUGHT -> ShipDurability(10, 2.5)
        }
index 28df75f45919e260a633d2972859831c3b9145c8..a3370901c962ebf72e3caf1008a3afb68451796c 100644 (file)
@@ -24,8 +24,8 @@ data class ShipInstance(
        val armaments: ShipInstanceArmaments = ship.armaments.instantiate(),
        val usedArmaments: Set<Id<ShipWeapon>> = emptySet(),
        
-       val fighterWings: List<ShipHangarWing> = emptyList(),
-       val bomberWings: List<ShipHangarWing> = emptyList(),
+       val fighterWings: Set<ShipHangarWing> = emptySet(),
+       val bomberWings: Set<ShipHangarWing> = emptySet(),
 ) {
        val id: Id<ShipInstance>
                get() = ship.id.reinterpret()
index 74bbf99927a1384df9b1aa31a7b4a175707cb698..2b4d7dd5aade78f39e98b6ee765c8a0ad5e83775 100644 (file)
@@ -16,6 +16,9 @@ enum class ShipWeightClass(
        COLOSSUS(5, 5),
        
        // Isarnareykk-specific
+       AUXILIARY_SHIP(1, 0),
+       LIGHT_CRUISER(2, 1),
+       MEDIUM_CRUISER(3, 2),
        HEAVY_CRUISER(4, 4),
        
        // Vestigium-specific
@@ -38,6 +41,9 @@ enum class ShipWeightClass(
                        GRAND_CRUISER -> 300
                        COLOSSUS -> 500
                        
+                       AUXILIARY_SHIP -> 50
+                       LIGHT_CRUISER -> 100
+                       MEDIUM_CRUISER -> 200
                        HEAVY_CRUISER -> 400
                        
                        FRIGATE -> 150
@@ -100,18 +106,18 @@ enum class ShipType(
        AEDON(Faction.MASRA_DRAETSEN, ShipWeightClass.COLOSSUS),
        
        // Isarnareykk
-       GANNAN(Faction.ISARNAREYKK, ShipWeightClass.ESCORT),
-       LODOVIK(Faction.ISARNAREYKK, ShipWeightClass.ESCORT),
-       
-       KARNAS(Faction.ISARNAREYKK, ShipWeightClass.DESTROYER),
-       PERTONA(Faction.ISARNAREYKK, ShipWeightClass.DESTROYER),
-       VOSS(Faction.ISARNAREYKK, ShipWeightClass.DESTROYER),
-       
-       BREKORYN(Faction.ISARNAREYKK, ShipWeightClass.CRUISER),
-       FALK(Faction.ISARNAREYKK, ShipWeightClass.CRUISER),
-       LORUS(Faction.ISARNAREYKK, ShipWeightClass.CRUISER),
-       ORSH(Faction.ISARNAREYKK, ShipWeightClass.CRUISER),
-       TEFRAN(Faction.ISARNAREYKK, ShipWeightClass.CRUISER),
+       GANNAN(Faction.ISARNAREYKK, ShipWeightClass.AUXILIARY_SHIP),
+       LODOVIK(Faction.ISARNAREYKK, ShipWeightClass.AUXILIARY_SHIP),
+       
+       KARNAS(Faction.ISARNAREYKK, ShipWeightClass.LIGHT_CRUISER),
+       PERTONA(Faction.ISARNAREYKK, ShipWeightClass.LIGHT_CRUISER),
+       VOSS(Faction.ISARNAREYKK, ShipWeightClass.LIGHT_CRUISER),
+       
+       BREKORYN(Faction.ISARNAREYKK, ShipWeightClass.MEDIUM_CRUISER),
+       FALK(Faction.ISARNAREYKK, ShipWeightClass.MEDIUM_CRUISER),
+       LORUS(Faction.ISARNAREYKK, ShipWeightClass.MEDIUM_CRUISER),
+       ORSH(Faction.ISARNAREYKK, ShipWeightClass.MEDIUM_CRUISER),
+       TEFRAN(Faction.ISARNAREYKK, ShipWeightClass.MEDIUM_CRUISER),
        
        KASSCK(Faction.ISARNAREYKK, ShipWeightClass.HEAVY_CRUISER),
        KHORR(Faction.ISARNAREYKK, ShipWeightClass.HEAVY_CRUISER),
index ea6cceba42b7fd0b688774e6e36458d920b7af7b..a45c14d368481e6be8bbf3132c2ff90c97a597ec 100644 (file)
@@ -21,6 +21,7 @@ enum class FiringArc {
        companion object {
                val FIRE_360: Set<FiringArc> = setOf(BOW, ABEAM_PORT, ABEAM_STARBOARD, STERN)
                val FIRE_BROADSIDE: Set<FiringArc> = setOf(ABEAM_PORT, ABEAM_STARBOARD)
+               val FIRE_FORE_270: Set<FiringArc> = setOf(BOW, ABEAM_PORT, ABEAM_STARBOARD)
        }
 }
 
@@ -321,9 +322,9 @@ fun ShipInstance.afterTargeted(by: ShipInstance, weaponId: Id<ShipWeapon>) = whe
        is ShipWeaponInstance.Hangar -> {
                ImpactResult.Damaged(
                        if (weapon.weapon.wing == StrikeCraftWing.FIGHTERS)
-                               copy(fighterWings = fighterWings + listOf(ShipHangarWing(by.id, weaponId)))
+                               copy(fighterWings = fighterWings + setOf(ShipHangarWing(by.id, weaponId)))
                        else
-                               copy(bomberWings = bomberWings + listOf(ShipHangarWing(by.id, weaponId)))
+                               copy(bomberWings = bomberWings + setOf(ShipHangarWing(by.id, weaponId)))
                )
        }
        is ShipWeaponInstance.Torpedo -> {
@@ -390,7 +391,14 @@ fun getWeaponPickRequest(weapon: ShipWeapon, position: ShipPosition, side: Globa
                
                PickRequest(
                        PickType.Ship(targetSet),
-                       PickBoundary.WeaponsFire(position.currentLocation, position.facingAngle, weapon.minRange, weapon.maxRange, weapon.firingArcs)
+                       PickBoundary.WeaponsFire(
+                               center = position.currentLocation,
+                               facing = position.facingAngle,
+                               minDistance = weapon.minRange,
+                               maxDistance = weapon.maxRange,
+                               firingArcs = weapon.firingArcs,
+                               canSelfSelect = side in targetSet
+                       )
                )
        }
 }
index c6cbbb63c4b399610028fcc1e17a57160dcfff4b..6141a2d5fcb602032085b2bd4389d40078147354 100644 (file)
@@ -69,7 +69,6 @@ fun mechyrdiaShipWeapons(
 
 fun diadochiShipWeapons(
        torpedoes: Int,
-       foreLances: Int,
        hasRevelationGun: Boolean,
        
        cannonSections: Int,
@@ -84,8 +83,6 @@ fun diadochiShipWeapons(
                idCounter.add(weapons, ShipWeapon.Torpedo(setOf(FiringArc.BOW), "Fore torpedo launchers"))
        }
        
-       idCounter.add(weapons, ShipWeapon.Lance(foreLances, setOf(FiringArc.BOW), "Fore lance battery"))
-       
        if (hasRevelationGun)
                idCounter.add(weapons, ShipWeapon.RevelationGun)
        
@@ -107,7 +104,7 @@ fun diadochiShipWeapons(
        }
        
        repeat(dorsalLances) {
-               idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_BROADSIDE, "Dorsal lance turrets"))
+               idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_FORE_270, "Dorsal lance batteries"))
        }
        
        return ShipArmaments(weapons)
index 190402270cc8a20622347cf5e328bbcfcc8c3f01..21caa7fd4d3c3fcc0b401ee7f5a988e7679705d5 100644 (file)
@@ -24,19 +24,19 @@ val ShipType.armaments: ShipArmaments
                ShipType.NOVA_ROMA -> mechyrdiaShipWeapons(3, false, 0, 3, 0, 1)
                ShipType.TYLA -> mechyrdiaShipWeapons(3, false, 1, 0, 2, 1)
                
-               ShipType.ERIS -> diadochiShipWeapons(2, 0, false, 1, 0, 0, 0)
-               ShipType.TYPHON -> diadochiShipWeapons(0, 2, false, 1, 0, 0, 0)
-               ShipType.AHRIMAN -> diadochiShipWeapons(1, 0, false, 0, 1, 0, 0)
-               ShipType.APOPHIS -> diadochiShipWeapons(1, 0, false, 0, 0, 1, 1)
-               ShipType.AZATHOTH -> diadochiShipWeapons(1, 0, false, 1, 0, 0, 0)
-               ShipType.CHERNOBOG -> diadochiShipWeapons(2, 0, false, 0, 2, 0, 0)
-               ShipType.CIPACTLI -> diadochiShipWeapons(2, 0, false, 2, 0, 0, 0)
-               ShipType.LOTAN -> diadochiShipWeapons(2, 0, false, 0, 0, 2, 2)
-               ShipType.MORGOTH -> diadochiShipWeapons(2, 0, false, 1, 1, 0, 0)
-               ShipType.TIAMAT -> diadochiShipWeapons(2, 0, false, 1, 0, 1, 1)
-               ShipType.CHARYBDIS -> diadochiShipWeapons(3, 0, false, 3, 0, 0, 0)
-               ShipType.SCYLLA -> diadochiShipWeapons(3, 0, false, 1, 0, 2, 0)
-               ShipType.AEDON -> diadochiShipWeapons(0, 0, true, 3, 0, 0, 0)
+               ShipType.ERIS -> diadochiShipWeapons(2, false, 1, 0, 0, 0)
+               ShipType.TYPHON -> diadochiShipWeapons(0, false, 1, 0, 0, 1)
+               ShipType.AHRIMAN -> diadochiShipWeapons(1, false, 0, 1, 0, 0)
+               ShipType.APOPHIS -> diadochiShipWeapons(1, false, 0, 0, 1, 1)
+               ShipType.AZATHOTH -> diadochiShipWeapons(1, false, 1, 0, 0, 0)
+               ShipType.CHERNOBOG -> diadochiShipWeapons(2, false, 0, 2, 0, 0)
+               ShipType.CIPACTLI -> diadochiShipWeapons(2, false, 2, 0, 0, 0)
+               ShipType.LOTAN -> diadochiShipWeapons(2, false, 0, 0, 2, 2)
+               ShipType.MORGOTH -> diadochiShipWeapons(2, false, 1, 1, 0, 0)
+               ShipType.TIAMAT -> diadochiShipWeapons(2, false, 1, 0, 1, 1)
+               ShipType.CHARYBDIS -> diadochiShipWeapons(3, false, 3, 0, 0, 0)
+               ShipType.SCYLLA -> diadochiShipWeapons(3, false, 1, 0, 2, 0)
+               ShipType.AEDON -> diadochiShipWeapons(0, true, 3, 0, 0, 0)
                
                ShipType.GANNAN -> fulkreykkShipWeapons(0, true, 0, 0)
                ShipType.LODOVIK -> fulkreykkShipWeapons(4, false, 0, 0)
index b43515f53b3d27e878ac800cd937a840afe2b016..3de91faeb90c9048283f9343de3a73f9301baa0a 100644 (file)
@@ -5,6 +5,7 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.isActive
 import kotlinx.serialization.json.Json
+import kotlin.math.exp
 import kotlin.math.roundToInt
 
 val jsonSerializer = Json {
@@ -38,3 +39,5 @@ else if (multiplier == 1) this
 else this + (this * (multiplier - 1))
 
 fun Double.toPercent() = "${(this * 100).roundToInt()}%"
+
+fun smoothNegative(x: Double) = if (x < 0) exp(x) else x + 1
index fbd8782362260f9061b90a09cdd14ca397eb9796..70736d7c8fda7fb1005ea33f96029221d9db8d31 100644 (file)
@@ -3,20 +3,28 @@ package starshipfights.game
 import io.ktor.client.*
 import io.ktor.client.engine.js.*
 import io.ktor.client.features.websocket.*
+import kotlinx.browser.document
 import kotlinx.browser.window
 import kotlinx.coroutines.CoroutineExceptionHandler
 import kotlinx.coroutines.MainScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.plus
 import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.json.decodeFromDynamic
+import org.w3c.dom.HTMLScriptElement
 
 val rootPathWs = "ws" + window.location.origin.removePrefix("http")
 
 @OptIn(ExperimentalSerializationApi::class)
 val clientMode: ClientMode = try {
-       jsonSerializer.decodeFromDynamic(ClientMode.serializer(), window.asDynamic().sfClientMode)
-} catch (_: Exception) {
+       jsonSerializer.decodeFromString(
+               ClientMode.serializer(),
+               document.getElementById("sf-client-mode").unsafeCast<HTMLScriptElement>().text
+       )
+} catch (ex: Exception) {
+       ex.printStackTrace()
+       ClientMode.Error("Invalid client mode sent from server")
+} catch (dyn: dynamic) {
+       console.error(dyn)
        ClientMode.Error("Invalid client mode sent from server")
 }
 
@@ -31,16 +39,18 @@ val httpClient = HttpClient(Js) {
 }
 
 fun main() {
+       window.addEventListener("beforeunload", { e ->
+               if (interruptExit) {
+                       e.preventDefault()
+                       e.asDynamic().returnValue = ""
+               }
+       })
+       
        AppScope.launch {
                Popup.LoadingScreen("Loading resources...") {
                        RenderResources.load(clientMode !is ClientMode.InGame)
                }.display()
                
-               window.addEventListener("beforeunload", { e ->
-                       e.preventDefault()
-                       e.asDynamic().returnValue = ""
-               })
-               
                when (clientMode) {
                        is ClientMode.MatchmakingMenu -> matchmakingMain(clientMode.admirals)
                        is ClientMode.InGame -> gameMain(clientMode.playerSide, clientMode.connectToken, clientMode.initialState)
@@ -48,3 +58,5 @@ fun main() {
                }
        }
 }
+
+var interruptExit: Boolean = false
index 4629b1126f9ff55693b89ccb83bdb1130bc18333..0fa1f7cfb3098181a6b260c33bfa32626f3859b5 100644 (file)
@@ -181,6 +181,8 @@ private fun CoroutineScope.uiResponder(actions: SendChannel<PlayerAction>, error
 }
 
 suspend fun gameMain(side: GlobalSide, token: String, state: GameState) {
+       interruptExit = true
+       
        initializePicking()
        
        mySide = side
@@ -201,6 +203,7 @@ suspend fun gameMain(side: GlobalSide, token: String, state: GameState) {
                val finalMessage = connectionJob.await()
                renderingJob.cancel()
                
+               interruptExit = false
                Popup.GameOver(finalMessage, gameState.value).display()
        }
 }
index a376af0d071f155091370ed673309d7e3734e975..cc06a63e9a61b625cf584231841d3761a8bfb316 100644 (file)
@@ -45,6 +45,8 @@ suspend fun setupBackground() {
 }
 
 private suspend fun enterGame(connectToken: String): Nothing {
+       interruptExit = false
+       
        document.body!!.append.form(action = "/play", method = FormMethod.post, encType = FormEncType.applicationXWwwFormUrlEncoded) {
                style = "display:none"
                hiddenInput {
@@ -116,6 +118,8 @@ private suspend fun usePlayerLogin(admirals: List<InGameAdmiral>) {
 }
 
 suspend fun matchmakingMain(admirals: List<InGameAdmiral>) {
+       interruptExit = true
+       
        coroutineScope {
                launch { setupBackground() }
                launch { usePlayerLogin(admirals) }
index 6dffa1c00b36dd57de95f65bd443966dbe66fb97..3264aad0ca3fcef38f655cf6b35a4f8bd6752f8f 100644 (file)
@@ -699,6 +699,7 @@ object GameUI {
                                                val firingArcsDesc = when (firingArcs) {
                                                        FiringArc.FIRE_360 -> "360-Degree"
                                                        FiringArc.FIRE_BROADSIDE -> "Broadside"
+                                                       FiringArc.FIRE_FORE_270 -> "Dorsal"
                                                        setOf(FiringArc.ABEAM_PORT) -> "Port"
                                                        setOf(FiringArc.ABEAM_STARBOARD) -> "Starboard"
                                                        setOf(FiringArc.BOW) -> "Fore"
@@ -748,6 +749,18 @@ object GameUI {
                                                                        }
                                                                }
                                                        }
+                                                       is PlayerAbilityType.RecallStrikeCraft -> {
+                                                               a(href = "#") {
+                                                                       +"Recall "
+                                                                       if (firingArcsDesc != null)
+                                                                               +"$firingArcsDesc "
+                                                                       +weaponDesc
+                                                                       onClickFunction = { e ->
+                                                                               e.preventDefault()
+                                                                               responder.useAbility(ability)
+                                                                       }
+                                                               }
+                                                       }
                                                }
                                        }
                                }
index 5f02b603fbe0202aa4af1015973226f4639bef3b..5c46ffab5f656bdbba93da95159c6ca1866b2641 100644 (file)
@@ -358,7 +358,7 @@ private fun PickBoundary.render(): List<Shape> {
                                        .unsafeCast<Shape>()
                        )
                }
-               is PickBoundary.AlongLine -> emptyList()
+               is PickBoundary.AlongLine -> emptyList() // Handled in a special case
                is PickBoundary.Rectangle -> listOf(
                        Shape()
                                .moveTo(RenderScaling.toWorldLength(center.vector.x + width2), RenderScaling.toWorldLength(center.vector.y + length2))
index 32c2bd47a46463b84a0c9988f8a9938790e78722..4d224669c4c9efa67c5def10d9e548f4acc58e1e 100644 (file)
@@ -45,10 +45,10 @@ fun ClientMode.view(): HTML.() -> Unit = {
                }
                
                script {
+                       attributes["id"] = "sf-client-mode"
+                       type = "application/json"
                        unsafe {
-                               +"window.sfClientMode = "
                                +jsonSerializer.encodeToString(ClientMode.serializer(), this@view)
-                               +";"
                        }
                }
                
index e50e48337c7ba1bc3b984682262c453d6250d80b..e1acb2a792eae41b0c40b44ff92bdbddba867148 100644 (file)
@@ -34,9 +34,7 @@ suspend fun ApplicationCall.aboutPage(): HTML.() -> Unit {
                "About", standardNavBar(), null
        ) {
                section {
-                       img(alt = "Starship Fights Logo", src = "/static/images/logo.svg") {
-                               style = "width:100%"
-                       }
+                       h1 { +"In Development" }
                        p {
                                +"This is a test instance of Starship Fights."
                        }
@@ -52,9 +50,7 @@ suspend fun ApplicationCall.aboutPage(): HTML.() -> Unit {
                )
        ) {
                section {
-                       img(alt = "Starship Fights Logo", src = "/static/images/logo.svg") {
-                               style = "width:100%"
-                       }
+                       h1 { +"About Starship Fights" }
                        p {
                                +"Starship Fights is designed and programmed by the person behind "
                                a(href = "https://nationstates.net/mechyrdia") { +"Mechyrdia" }
index 636cc9fa98e9225d9d9402670be4ce8354e65176..050ed72fd16ed58a234652278dab97fdd9521a55 100644 (file)
@@ -83,42 +83,38 @@ suspend fun ApplicationCall.shipPage(shipType: ShipType): HTML.() -> Unit = page
                
                table {
                        tr {
-                               th {
-                                       +"Weight Class"
-                                       br
-                                       +"(Point Cost)"
-                               }
-                               th {
-                                       +"Hull Integrity"
-                               }
-                               th { +"Max Acceleration" }
-                               th { +"Max Rotation" }
-                               th {
-                                       +"Reactor Power"
-                                       br
-                                       +"(Per Subsystem)"
-                               }
-                               th { +"Energy Flow" }
+                               th { +"Weight Class" }
+                               th { +"Hull Integrity" }
+                               th { +"Defense Turrets" }
                        }
                        tr {
                                td {
                                        +shipType.weightClass.displayName
                                        br
-                                       +"(${shipType.weightClass.basePointCost})"
+                                       +"(${shipType.weightClass.basePointCost} points to deploy)"
                                }
                                td {
                                        +"${shipType.weightClass.durability.maxHullPoints} impacts"
                                }
                                td {
-                                       +"${shipType.weightClass.movement.moveSpeed.roundToInt()} meters/turn"
+                                       +"${shipType.weightClass.durability.turretDefense.toPercent()} fighter-wing equivalent"
                                }
+                       }
+                       tr {
+                               th { +"Max Movement" }
+                               th { +"Reactor Power" }
+                               th { +"Energy Flow" }
+                       }
+                       tr {
                                td {
-                                       +"${(shipType.weightClass.movement.turnAngle * 180.0 / PI).roundToInt()} degrees/turn"
+                                       +"Accelerate ${shipType.weightClass.movement.moveSpeed.roundToInt()} meters/turn"
+                                       br
+                                       +"Rotate ${(shipType.weightClass.movement.turnAngle * 180.0 / PI).roundToInt()} degrees/turn"
                                }
                                td {
                                        +shipType.weightClass.reactor.powerOutput.toString()
                                        br
-                                       +"(${shipType.weightClass.reactor.subsystemAmount})"
+                                       +"(${shipType.weightClass.reactor.subsystemAmount} per subsystem)"
                                }
                                td {
                                        +shipType.weightClass.reactor.gridEfficiency.toString()
index 715b8ee17bc2e7117203618dd42bb46648cee3f6..e56fae71d66ded822bf51fb8504c65ccd9862eb3 100644 (file)
@@ -8,16 +8,13 @@
        <path
                        style="fill:#2255bb;fill-opacity:1"
                        id="path12"
-                       d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"
-                       fill="#36b"/>
+                       d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
        <path
                        style="fill:#4477dd;fill-opacity:1"
                        id="path10"
-                       d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"
-                       fill="#36b"/>
+                       d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
        <path
                        style="fill:#6699ff;fill-opacity:1"
                        id="path14"
-                       d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"
-                       fill="#15a5ea"/>
+                       d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"/>
 </svg>