Add ship boarding
authorTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 4 Jun 2022 22:11:33 +0000 (18:11 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Sat, 4 Jun 2022 22:11:33 +0000 (18:11 -0400)
19 files changed:
plan/icons/assault-action.svg [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/ai/ai_behaviors.kt
src/commonMain/kotlin/starshipfights/game/ai/ai_util_combat.kt
src/commonMain/kotlin/starshipfights/game/ai/ai_util_nav.kt
src/commonMain/kotlin/starshipfights/game/game_ability.kt
src/commonMain/kotlin/starshipfights/game/game_chat.kt
src/commonMain/kotlin/starshipfights/game/game_initiative.kt
src/commonMain/kotlin/starshipfights/game/game_state.kt
src/commonMain/kotlin/starshipfights/game/ship.kt
src/commonMain/kotlin/starshipfights/game/ship_boarding.kt [new file with mode: 0644]
src/commonMain/kotlin/starshipfights/game/ship_instances.kt
src/commonMain/kotlin/starshipfights/game/ship_modules.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/jsMain/kotlin/starshipfights/game/game_ui.kt
src/jsMain/resources/images/assault-action.svg [new file with mode: 0644]
src/jsMain/resources/images/defense-action.svg [new file with mode: 0644]
src/jvmMain/kotlin/starshipfights/info/views_ships.kt

diff --git a/plan/icons/assault-action.svg b/plan/icons/assault-action.svg
new file mode 100644 (file)
index 0000000..70d13e9
--- /dev/null
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="210mm"
+   height="297mm"
+   viewBox="0 0 210 297"
+   version="1.1"
+   id="svg8"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
+   sodipodi:docname="assault-action.svg">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.7"
+     inkscape:cx="393.20743"
+     inkscape:cy="496.11075"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:snap-bbox="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#0f0008;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.98437512;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       d="m 64,169.00006 18.488795,35.55545 62.578095,62.57706 -1.84175,1.84227 c -3.01631,-1.27869 -6.50655,-0.60084 -8.82479,1.7141 -3.14191,3.1419 -3.14191,8.23571 0,11.3776 3.14189,3.14192 8.2357,3.14192 11.3776,0 2.31539,-2.31824 2.99315,-5.8088 1.7141,-8.8253 l 5.3971,-5.39708 22.04413,19.45152 V 297 h 17.06666 v -17.06666 h -9.70484 l -19.4505,-22.04465 5.39711,-5.39709 c 3.01633,1.27895 6.5069,0.60139 8.82528,-1.71359 3.14193,-3.14189 3.14193,-8.23622 0,-11.37811 -3.1419,-3.14192 -8.23621,-3.14192 -11.37812,0 -2.31528,2.31813 -2.99339,5.80881 -1.7146,8.8253 l -1.84073,1.84123 -62.578089,-62.57757 z m 127.99994,0 -35.55545,18.48931 -25.59999,25.59999 7.11119,7.11119 22.75571,-22.7552 2.84427,2.84427 -22.75571,22.7552 7.11119,7.1112 25.6,-25.59999 z m -96.710895,28.44426 56.889045,56.88903 -2.84427,2.84427 -56.889046,-56.88903 z m 12.800765,38.40024 -14.222382,14.22239 -1.841748,-1.84175 c 1.278689,-3.01631 0.600831,-6.50655 -1.714109,-8.82479 -3.141898,-3.14191 -8.236225,-3.14191 -11.378115,0 -3.141922,3.14189 -3.141922,8.2357 0,11.3776 2.31824,2.31538 5.809316,2.99366 8.825817,1.71463 l 5.396569,5.39657 -19.451007,22.04413 H 64.000516 L 64,297 h 17.067175 l -5.16e-4,-9.70432 22.044651,-19.45101 5.39708,5.39709 c -1.27895,3.01633 -0.60086,6.50691 1.71411,8.8253 3.14189,3.14192 8.23571,3.14191 11.3776,0 3.14191,-3.1419 3.14192,-8.2357 0,-11.3776 -2.31814,-2.3153 -5.80881,-2.99339 -8.8253,-1.71462 l -1.84123,-1.84123 14.2229,-14.22239 v -5.2e-4 l -7.11119,-7.11068 -11.37812,11.37812 -2.84427,-2.84427 11.37812,-11.37812 z m -23.467296,4.71909 c 1.158166,0 2.316149,0.44185 3.199804,1.3255 1.76729,1.76731 1.76729,4.63282 0,6.40013 -1.76731,1.76729 -4.632817,1.76729 -6.400125,0 -1.7673,-1.76731 -1.7673,-4.63282 0,-6.40013 0.883656,-0.88365 2.042155,-1.3255 3.200321,-1.3255 z m 86.755436,0 c 1.15816,0 2.31616,0.44185 3.19979,1.3255 1.76731,1.76731 1.76731,4.6323 0,6.39961 -1.76729,1.76728 -4.6323,1.7673 -6.39961,0 -1.76729,-1.76731 -1.76729,-4.63231 0,-6.39961 0.88365,-0.88365 2.04166,-1.3255 3.19982,-1.3255 z m -31.28905,31.28853 c 1.15816,0 2.31666,0.44186 3.20032,1.3255 1.76729,1.76731 1.76729,4.63282 0,6.40013 -1.76731,1.7673 -4.63282,1.7673 -6.40013,0 -1.7673,-1.76731 -1.7673,-4.63282 0,-6.40013 0.88366,-0.88364 2.04164,-1.3255 3.19981,-1.3255 z m -24.17786,5.2e-4 c 1.15817,0 2.31667,0.44186 3.20032,1.3255 1.76731,1.76731 1.7673,4.63282 0,6.40012 -1.7673,1.7673 -4.63281,1.76729 -6.40012,0 -1.76729,-1.7673 -1.7673,-4.63281 0,-6.40012 0.88365,-0.88365 2.04164,-1.32551 3.1998,-1.3255 z m -38.399719,11.63598 v 9.95598 h -9.955467 v -9.95547 z m 100.977299,0 h 9.95545 v 9.95598 l -9.95494,-5.2e-4 z"
+       id="rect10-6-0"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:#000008;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.98437512;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"
+       d="m 29.999722,117.00006 -11.999787,37.9999 5.18e-4,87.99972 h -2.590024 c -1.22176,-3.01992 -4.152697,-4.99739 -7.4104,-4.9997 -4.418301,-2e-5 -8.000045999939,3.58173 -8.000029,8.00003 -1.6999939e-5,4.4183 3.581728,8.00005 8.000029,8.00003 3.258017,-0.002 6.189248,-1.97991 7.410918,-5.00021 h 7.5892 l 1.823143,29.17651 -6.823355,6.82336 11.999787,12.0003 12.0003,-12.0003 -6.82388,-6.82336 1.82367,-29.17651 h 7.58971 c 1.2216,3.02012 4.15258,4.99781 7.4104,5.00021 4.41831,2e-5 8.00005,-3.58173 8.00003,-8.00003 2e-5,-4.4183 -3.58172,-8.00005 -8.00003,-8.00003 -3.25789,0.002 -6.18908,1.9796 -7.41091,4.9997 h -2.58899 v -87.99972 z m -1.999363,42.00001 h 3.999753 v 79.99977 h -3.999753 z m -20.00033,82.49996 c 2.485267,1e-5 4.499972,2.01471 4.499983,4.49998 -1.1e-5,2.48527 -2.014716,4.49997 -4.499983,4.49998 -2.485267,-1e-5 -4.499972,-2.01471 -4.499983,-4.49998 1.1e-5,-2.48527 2.014716,-4.49997 4.499983,-4.49998 z m 43.999893,0 c 2.48527,1e-5 4.49998,2.01471 4.49999,4.49998 -1e-5,2.48527 -2.01472,4.49997 -4.49999,4.49998 -2.48526,-1e-5 -4.49997,-2.01471 -4.49998,-4.49998 1e-5,-2.48527 2.01472,-4.49997 4.49998,-4.49998 z m -22.0002,36.49958 7.00009,7.00009 -7.00009,7.00009 -6.999575,-7.00009 z"
+       id="rect10"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccc" />
+    <path
+       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:16;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:8;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       d="M 128,25.001017 A 127.99955,179.13738 0 0 1 64,49.000494 127.99954,120.08843 0 0 0 128,153 127.99954,120.08843 0 0 0 192,49.000494 127.99955,179.13738 0 0 1 128,25.001017 Z m 0.001,19.367213 v 89.78543 C 102.23016,117.43984 85.38685,91.83793 81.126541,63.415064 97.60824,60.748673 113.4133,54.119556 128.00104,44.36823 Z"
+       id="rect849-4"
+       inkscape:connector-curvature="0" />
+  </g>
+</svg>
index 8384b75fdffaaa712fcffac0a7b8628640634395..e025c6ad3b72384bda758700da90ba1986be408f 100644 (file)
@@ -60,6 +60,13 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                                if (targetedShip.owner != mySide)
                                                                        brain[shipAttackPriority forShip targetedShip.id] += instincts[combatFrustratedByFailedAttacks]
                                                        }
+                                                       is ChatEntry.ShipBoarded -> {
+                                                               val targetedShip = state.ships[msg.ship] ?: continue
+                                                               if (targetedShip.owner != mySide)
+                                                                       brain[shipAttackPriority forShip targetedShip.id] -= Random.nextDouble(msg.damageAmount - 0.5, msg.damageAmount + 0.5) * instincts[combatForgiveTarget]
+                                                               else
+                                                                       brain[shipAttackPriority forShip msg.boarder] += Random.nextDouble(msg.damageAmount - 0.5, msg.damageAmount + 0.5) * instincts[combatAvengeAttacks]
+                                                       }
                                                        is ChatEntry.ShipDestroyed -> {
                                                                val targetedShip = state.ships[msg.ship] ?: continue
                                                                if (targetedShip.owner == mySide && msg.destroyedBy is ShipAttacker.EnemyShip)
@@ -71,8 +78,8 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                        }
                        
                        launch(onGameEnd) {
-                               for ((phase, canAct) in phasePipe) {
-                                       if (!canAct) continue
+                               loop@ for ((phase, canAct) in phasePipe) {
+                                       if (!canAct) continue@loop
                                        
                                        val state = gameState.value
                                        
@@ -92,13 +99,11 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                        doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
                                                }
                                                is GamePhase.Power -> {
-                                                       val repowerableShips = state.ships.values.filter { ship ->
+                                                       val powerableShips = state.ships.values.filter { ship ->
                                                                ship.owner == mySide && !ship.isDoneCurrentPhase
                                                        }
                                                        
-                                                       if (repowerableShips.isEmpty())
-                                                               doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
-                                                       else for (ship in repowerableShips)
+                                                       for (ship in powerableShips)
                                                                when (val reactor = ship.ship.reactor) {
                                                                        FelinaeShipReactor -> {
                                                                                val newPowerMode = if (ship.hullAmount < ship.durability.maxHullPoints)
@@ -127,6 +132,8 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                                                doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DistributePower(ship.id), PlayerAbilityData.DistributePower(chosenPower)))
                                                                        }
                                                                }
+                                                       
+                                                       doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
                                                }
                                                is GamePhase.Move -> {
                                                        val movableShips = state.ships.values.filter { ship ->
@@ -135,31 +142,32 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                        
                                                        val smallestShipTier = movableShips.minOfOrNull { ship -> ship.ship.shipType.weightClass.tier }
                                                        
-                                                       if (smallestShipTier == null)
+                                                       if (smallestShipTier == null) {
                                                                doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
-                                                       else {
-                                                               val movableSmallestShips = movableShips.filter { ship ->
-                                                                       ship.ship.shipType.weightClass.tier == smallestShipTier
-                                                               }
-                                                               
-                                                               val moveThisShip = movableSmallestShips.associateWith { it.calculateSuffering() + 1.0 }.weightedRandom()
-                                                               doActions.send(navigate(state, moveThisShip, instincts, brain))
-                                                               
-                                                               withTimeoutOrNull(50L) { getErrors.receive() }?.let { error ->
-                                                                       logWarning("Error when moving ship ID ${moveThisShip.id} - $error")
-                                                                       doActions.send(
-                                                                               PlayerAction.UseAbility(
-                                                                                       PlayerAbilityType.MoveShip(moveThisShip.id),
-                                                                                       PlayerAbilityData.MoveShip(moveThisShip.position)
-                                                                               )
+                                                               continue@loop
+                                                       }
+                                                       
+                                                       val movableSmallestShips = movableShips.filter { ship ->
+                                                               ship.ship.shipType.weightClass.tier == smallestShipTier
+                                                       }
+                                                       
+                                                       val moveThisShip = movableSmallestShips.associateWith { it.calculateSuffering() + 1.0 }.weightedRandom()
+                                                       doActions.send(navigate(state, moveThisShip, instincts, brain))
+                                                       
+                                                       withTimeoutOrNull(50L) { getErrors.receive() }?.let { error ->
+                                                               logWarning("Error when moving ship ID ${moveThisShip.id} - $error")
+                                                               doActions.send(
+                                                                       PlayerAction.UseAbility(
+                                                                               PlayerAbilityType.MoveShip(moveThisShip.id),
+                                                                               PlayerAbilityData.MoveShip(moveThisShip.position)
                                                                        )
-                                                               }
+                                                               )
                                                        }
                                                }
                                                is GamePhase.Attack -> {
                                                        val potentialAttacks = state.ships.values.flatMap { ship ->
                                                                if (ship.owner == mySide)
-                                                                       ship.armaments.weaponInstances.keys.filter {
+                                                                       ship.armaments.keys.filter {
                                                                                ship.canUseWeapon(it)
                                                                        }.flatMap { weaponId ->
                                                                                weaponId.validTargets(state, ship).map { target ->
@@ -171,44 +179,74 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                                weaponId.expectedAdvantageFromWeaponUsage(state, ship, target) * smoothNegative(brain[shipAttackPriority forShip target.id].signedPow(instincts[combatPrioritization])) * (1 + target.calculateSuffering()).signedPow(instincts[combatPreyOnTheWeak])
                                                        }
                                                        
-                                                       val attackWith = potentialAttacks.weightedRandomOrNull()
-                                                       
-                                                       if (attackWith == null)
-                                                               doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
-                                                       else {
-                                                               val (ship, weaponId, target) = attackWith
-                                                               val targetPickResponse = when (val weaponSpec = ship.armaments.weaponInstances[weaponId]?.weapon) {
-                                                                       is AreaWeapon -> {
-                                                                               val pickRequest = ship.getWeaponPickRequest(weaponSpec)
-                                                                               val targetLocation = target.position.location
-                                                                               val closestValidLocation = pickRequest.boundary.closestPointTo(targetLocation)
-                                                                               
-                                                                               val chosenLocation = if ((targetLocation - closestValidLocation).length >= EPSILON)
-                                                                                       closestValidLocation + ((closestValidLocation - targetLocation) * 0.2)
-                                                                               else closestValidLocation
-                                                                               
-                                                                               PickResponse.Location(chosenLocation)
-                                                                       }
-                                                                       else -> PickResponse.Ship(target.id)
+                                                       if (potentialAttacks.isEmpty() || Random.nextInt(3) == 0) {
+                                                               val potentialBoardings = state.ships.values.flatMap { ship ->
+                                                                       if (ship.owner == mySide && ship.canSendBoardingParty) {
+                                                                               val pickRequest = ship.getBoardingPickRequest()
+                                                                               state.ships.values.filter { target ->
+                                                                                       target.owner == mySide.other && target.position.location in pickRequest.boundary
+                                                                               }.map { target -> ship to target }
+                                                                       } else emptyList()
+                                                               }.associateWith { (ship, target) ->
+                                                                       ship.expectedBoardingSuccess(target)
                                                                }
                                                                
-                                                               doActions.send(PlayerAction.UseAbility(PlayerAbilityType.UseWeapon(ship.id, weaponId), PlayerAbilityData.UseWeapon(targetPickResponse)))
+                                                               val board = potentialBoardings.weightedRandomOrNull()
                                                                
-                                                               withTimeoutOrNull(50L) { getErrors.receive() }?.let { error ->
-                                                                       logWarning("Error when attacking target ship ID ${target.id} with weapon $weaponId of ship ID ${ship.id} - $error")
-                                                                       
-                                                                       val remainingAllAreaWeapons = potentialAttacks.keys.map { (attacker, weaponId, _) ->
-                                                                               attacker to weaponId
-                                                                       }.toSet().all { (attacker, weaponId) ->
-                                                                               attacker.armaments.weaponInstances[weaponId]?.weapon is AreaWeapon
-                                                                       }
+                                                               if (board != null) {
+                                                                       val (ship, target) = board
+                                                                       doActions.send(PlayerAction.UseAbility(PlayerAbilityType.BoardingParty(ship.id), PlayerAbilityData.BoardingParty(target.id)))
                                                                        
-                                                                       if (remainingAllAreaWeapons)
-                                                                               doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
-                                                                       else {
+                                                                       withTimeoutOrNull(50L) { getErrors.receive() }?.let { error ->
+                                                                               logWarning("Error when boarding target ship ID ${target.id} with assault parties of ship ID ${ship.id} - $error")
+                                                                               
                                                                                val nextState = gameState.value
                                                                                phasePipe.send(nextState.phase to (nextState.doneWithPhase != mySide && (!nextState.phase.usesInitiative || nextState.currentInitiative != mySide.other)))
                                                                        }
+                                                                       
+                                                                       continue@loop
+                                                               }
+                                                       }
+                                                       
+                                                       val attackWith = potentialAttacks.weightedRandomOrNull()
+                                                       
+                                                       if (attackWith == null) {
+                                                               doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
+                                                               continue@loop
+                                                       }
+                                                       
+                                                       val (ship, weaponId, target) = attackWith
+                                                       val targetPickResponse = when (val weaponSpec = ship.armaments[weaponId]?.weapon) {
+                                                               is AreaWeapon -> {
+                                                                       val pickRequest = ship.getWeaponPickRequest(weaponSpec)
+                                                                       val targetLocation = target.position.location
+                                                                       val closestValidLocation = pickRequest.boundary.closestPointTo(targetLocation)
+                                                                       
+                                                                       val chosenLocation = if ((targetLocation - closestValidLocation).length >= EPSILON)
+                                                                               closestValidLocation + ((closestValidLocation - targetLocation) * 0.2)
+                                                                       else closestValidLocation
+                                                                       
+                                                                       PickResponse.Location(chosenLocation)
+                                                               }
+                                                               else -> PickResponse.Ship(target.id)
+                                                       }
+                                                       
+                                                       doActions.send(PlayerAction.UseAbility(PlayerAbilityType.UseWeapon(ship.id, weaponId), PlayerAbilityData.UseWeapon(targetPickResponse)))
+                                                       
+                                                       withTimeoutOrNull(50L) { getErrors.receive() }?.let { error ->
+                                                               logWarning("Error when attacking target ship ID ${target.id} with weapon $weaponId of ship ID ${ship.id} - $error")
+                                                               
+                                                               val remainingAllAreaWeapons = potentialAttacks.keys.map { (attacker, weaponId, _) ->
+                                                                       attacker to weaponId
+                                                               }.toSet().all { (attacker, weaponId) ->
+                                                                       attacker.armaments[weaponId]?.weapon is AreaWeapon
+                                                               }
+                                                               
+                                                               if (remainingAllAreaWeapons)
+                                                                       doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
+                                                               else {
+                                                                       val nextState = gameState.value
+                                                                       phasePipe.send(nextState.phase to (nextState.doneWithPhase != mySide && (!nextState.phase.usesInitiative || nextState.currentInitiative != mySide.other)))
                                                                }
                                                        }
                                                }
@@ -217,17 +255,18 @@ suspend fun AIPlayer.behave(instincts: Instincts, mySide: GlobalSide) {
                                                                it !is PlayerAbilityType.DonePhase
                                                        }.randomOrNull()
                                                        
-                                                       if (repairAbility == null)
+                                                       if (repairAbility == null) {
                                                                doActions.send(PlayerAction.UseAbility(PlayerAbilityType.DonePhase(phase), PlayerAbilityData.DonePhase))
-                                                       else {
-                                                               when (repairAbility) {
-                                                                       is PlayerAbilityType.RepairShipModule -> PlayerAbilityData.RepairShipModule
-                                                                       is PlayerAbilityType.ExtinguishFire -> PlayerAbilityData.ExtinguishFire
-                                                                       is PlayerAbilityType.Recoalesce -> PlayerAbilityData.Recoalesce
-                                                                       else -> null
-                                                               }?.let { repairData ->
-                                                                       doActions.send(PlayerAction.UseAbility(repairAbility, repairData))
-                                                               }
+                                                               continue@loop
+                                                       }
+                                                       
+                                                       when (repairAbility) {
+                                                               is PlayerAbilityType.RepairShipModule -> PlayerAbilityData.RepairShipModule
+                                                               is PlayerAbilityType.ExtinguishFire -> PlayerAbilityData.ExtinguishFire
+                                                               is PlayerAbilityType.Recoalesce -> PlayerAbilityData.Recoalesce
+                                                               else -> null
+                                                       }?.let { repairData ->
+                                                               doActions.send(PlayerAction.UseAbility(repairAbility, repairData))
                                                        }
                                                }
                                        }
index e8050c028b4b99d6cf7f0dcd9a3e06ecd590ea9d..926ed085012e8fae1c2a7f9db53fb913d79cde82 100644 (file)
@@ -1,9 +1,6 @@
 package starshipfights.game.ai
 
-import starshipfights.game.FelinaeShipReactor
-import starshipfights.game.ShipInstance
-import starshipfights.game.ShipModuleStatus
-import starshipfights.game.durability
+import starshipfights.game.*
 
 val combatTargetShipWeight by instinct(0.5..2.5)
 
@@ -30,3 +27,7 @@ fun ShipInstance.calculateSuffering(): Double {
                }
        }
 }
+
+fun ShipInstance.expectedBoardingSuccess(against: ShipInstance): Double {
+       return smoothNegative((assaultModifier - against.defenseModifier).toDouble())
+}
index fe7b662e97cf683b519218df523f66a65272494c..dd1c8d1561e09e3a4a0d8491d58f5d1fc61ea588 100644 (file)
@@ -40,7 +40,7 @@ fun ShipInstance.canAttackWithDamage(gameState: GameState): Map<Id<ShipInstance>
 }
 
 fun ShipInstance.attackableTargets(gameState: GameState): Map<Id<ShipInstance>, Set<Id<ShipWeapon>>> {
-       return armaments.weaponInstances.keys.associateWith { weaponId ->
+       return armaments.keys.associateWith { weaponId ->
                weaponId.validTargets(gameState, this).map { it.id }.toSet()
        }.transpose()
 }
@@ -57,14 +57,14 @@ fun ShipInstance.attackableWithDamageBy(gameState: GameState): Map<Id<ShipInstan
 
 fun Id<ShipWeapon>.validTargets(gameState: GameState, ship: ShipInstance): List<ShipInstance> {
        if (!ship.canUseWeapon(this)) return emptyList()
-       val weaponInstance = ship.armaments.weaponInstances[this] ?: return emptyList()
+       val weaponInstance = ship.armaments[this] ?: return emptyList()
        
        return gameState.getValidTargets(ship, weaponInstance)
 }
 
 fun Id<ShipWeapon>.expectedAdvantageFromWeaponUsage(gameState: GameState, ship: ShipInstance, target: ShipInstance): Double {
        if (!ship.canUseWeapon(this)) return 0.0
-       val weaponInstance = ship.armaments.weaponInstances[this] ?: return 0.0
+       val weaponInstance = ship.armaments[this] ?: return 0.0
        val mustBeSameSide = weaponInstance is ShipWeaponInstance.Hangar && weaponInstance.weapon.wing == StrikeCraftWing.FIGHTERS
        if ((ship.owner == target.owner) != mustBeSameSide) return 0.0
        
index 04f7745caa2e206f97bc670d9a6f0baea618875c..3d23d6a32693ca741b26ca2a4e2187ee4b492c93 100644 (file)
@@ -417,7 +417,7 @@ sealed class PlayerAbilityType {
                        if (shipInstance.weaponAmount <= 0) return null
                        if (weapon in shipInstance.usedArmaments) return null
                        
-                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
+                       val shipWeapon = shipInstance.armaments[weapon] ?: return null
                        if (shipWeapon !is ShipWeaponInstance.Lance) return null
                        
                        return PlayerAbilityData.ChargeLance
@@ -430,7 +430,7 @@ sealed class PlayerAbilityType {
                        if (shipInstance.weaponAmount <= 0) return GameEvent.InvalidAction("Not enough power to charge lances")
                        if (weapon in shipInstance.usedArmaments) return GameEvent.InvalidAction("Cannot charge used lances")
                        
-                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
+                       val shipWeapon = shipInstance.armaments[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
                        if (shipWeapon !is ShipWeaponInstance.Lance) return GameEvent.InvalidAction("Cannot charge non-lance weapons")
                        
                        return GameEvent.StateChange(
@@ -438,10 +438,8 @@ sealed class PlayerAbilityType {
                                        ships = gameState.ships + mapOf(
                                                ship to shipInstance.copy(
                                                        weaponAmount = shipInstance.weaponAmount - 1,
-                                                       armaments = ShipInstanceArmaments(
-                                                               weaponInstances = shipInstance.armaments.weaponInstances + mapOf(
-                                                                       weapon to shipWeapon.copy(numCharges = shipWeapon.numCharges + shipInstance.firepower.lanceCharging)
-                                                               )
+                                                       armaments = shipInstance.armaments + mapOf(
+                                                               weapon to shipWeapon.copy(numCharges = shipWeapon.numCharges + shipInstance.firepower.lanceCharging)
                                                        )
                                                )
                                        )
@@ -458,7 +456,7 @@ sealed class PlayerAbilityType {
                        val shipInstance = gameState.ships[ship] ?: return null
                        if (!shipInstance.canUseWeapon(weapon)) return null
                        
-                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
+                       val shipWeapon = shipInstance.armaments[weapon] ?: return null
                        
                        val pickResponse = pick(shipInstance.getWeaponPickRequest(shipWeapon.weapon))
                        
@@ -473,7 +471,7 @@ sealed class PlayerAbilityType {
                        val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That attacking ship does not exist")
                        if (!shipInstance.canUseWeapon(weapon)) return GameEvent.InvalidAction("That weapon cannot be used")
                        
-                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
+                       val shipWeapon = shipInstance.armaments[weapon] ?: return GameEvent.InvalidAction("That weapon does not exist")
                        
                        val pickRequest = shipInstance.getWeaponPickRequest(shipWeapon.weapon)
                        val pickResponse = data.target
@@ -492,7 +490,7 @@ sealed class PlayerAbilityType {
                        val shipInstance = gameState.ships[ship] ?: return null
                        if (weapon !in shipInstance.usedArmaments) return null
                        
-                       val shipWeapon = shipInstance.armaments.weaponInstances[weapon] ?: return null
+                       val shipWeapon = shipInstance.armaments[weapon] ?: return null
                        if (shipWeapon !is ShipWeaponInstance.Hangar) return null
                        
                        return PlayerAbilityData.RecallStrikeCraft
@@ -504,7 +502,7 @@ sealed class PlayerAbilityType {
                        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")
+                       val shipWeapon = shipInstance.armaments[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)
@@ -521,7 +519,7 @@ sealed class PlayerAbilityType {
                                                        bomberWings = targetShip.bomberWings - hangarWing,
                                                )
                                        } + mapOf(ship to newShip)
-                               )
+                               ).withRecalculatedInitiative { calculateAttackPhaseInitiative() }
                        )
                }
        }
@@ -559,14 +557,12 @@ sealed class PlayerAbilityType {
                        val changedShips = hangars.groupBy { it.ship }.mapNotNull { (shipId, hangarWings) ->
                                val changedShip = gameState.ships[shipId] ?: return@mapNotNull null
                                changedShip.copy(
-                                       armaments = ShipInstanceArmaments(
-                                               changedShip.armaments.weaponInstances + hangarWings.associate {
-                                                       it.hangar to ShipWeaponInstance.Hangar(
-                                                               changedShip.ship.armaments.weapons[it.hangar] as ShipWeapon.Hangar,
-                                                               0.0
-                                                       )
-                                               }
-                                       )
+                                       armaments = changedShip.armaments + hangarWings.associate {
+                                               it.hangar to ShipWeaponInstance.Hangar(
+                                                       changedShip.ship.armaments[it.hangar] as ShipWeapon.Hangar,
+                                                       0.0
+                                               )
+                                       }
                                )
                        }.associateBy { it.id } + mapOf(
                                ship to shipInstance.copy(
@@ -578,7 +574,52 @@ sealed class PlayerAbilityType {
                        return GameEvent.StateChange(
                                gameState.copy(
                                        ships = gameState.ships + changedShips
-                               )
+                               ).withRecalculatedInitiative { calculateAttackPhaseInitiative() }
+                       )
+               }
+       }
+       
+       @Serializable
+       data class BoardingParty(override val ship: Id<ShipInstance>) : PlayerAbilityType(), ShipAbility {
+               override suspend fun beginOnClient(gameState: GameState, playerSide: GlobalSide, pick: suspend (PickRequest) -> PickResponse?): PlayerAbilityData? {
+                       if (gameState.phase !is GamePhase.Attack) return null
+                       
+                       val shipInstance = gameState.ships[ship] ?: return null
+                       if (!shipInstance.canSendBoardingParty) return null
+                       
+                       val pickResponse = pick(shipInstance.getBoardingPickRequest()) as? PickResponse.Ship ?: return null
+                       return PlayerAbilityData.BoardingParty(pickResponse.id)
+               }
+               
+               override fun finishOnServer(gameState: GameState, playerSide: GlobalSide, data: PlayerAbilityData): GameEvent {
+                       if (data !is PlayerAbilityData.BoardingParty) return GameEvent.InvalidAction("Internal error from using player ability")
+                       
+                       if (gameState.phase !is GamePhase.Attack) return GameEvent.InvalidAction("Ships can only send Boarding Parties during Phase III")
+                       
+                       val shipInstance = gameState.ships[ship] ?: return GameEvent.InvalidAction("That ship does not exist")
+                       if (!shipInstance.canSendBoardingParty) return GameEvent.InvalidAction("Cannot send a boarding party")
+                       
+                       val afterBoarding = shipInstance.afterBoarding() ?: return GameEvent.InvalidAction("Cannot send a boarding party")
+                       
+                       val boarded = gameState.ships[data.target] ?: return GameEvent.InvalidAction("That ship does not exist")
+                       val afterBoarded = shipInstance.board(boarded)
+                       
+                       val newShips = (if (afterBoarded is ImpactResult.Damaged)
+                               gameState.ships + mapOf(data.target to afterBoarded.ship)
+                       else gameState.ships - data.target) + mapOf(ship to afterBoarding)
+                       
+                       val newWrecks = gameState.destroyedShips + (if (afterBoarded is ImpactResult.Destroyed)
+                               mapOf(data.target to afterBoarded.ship)
+                       else emptyMap())
+                       
+                       val newChatEntries = gameState.chatBox + reportBoardingResult(afterBoarded, ship)
+                       
+                       return GameEvent.StateChange(
+                               gameState.copy(
+                                       ships = newShips,
+                                       destroyedShips = newWrecks,
+                                       chatBox = newChatEntries
+                               ).withRecalculatedInitiative { calculateAttackPhaseInitiative() }
                        )
                }
        }
@@ -743,6 +784,9 @@ sealed class PlayerAbilityData {
        @Serializable
        object DisruptionPulse : PlayerAbilityData()
        
+       @Serializable
+       data class BoardingParty(val target: Id<ShipInstance>) : PlayerAbilityData()
+       
        @Serializable
        object RepairShipModule : PlayerAbilityData()
        
@@ -820,7 +864,7 @@ else when (phase) {
                val chargeableLances = ships
                        .filterValues { it.owner == forPlayer && it.weaponAmount > 0 }
                        .flatMap { (id, ship) ->
-                               ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
+                               ship.armaments.mapNotNull { (weaponId, weapon) ->
                                        PlayerAbilityType.ChargeLance(id, weaponId).takeIf {
                                                when (weapon) {
                                                        is ShipWeaponInstance.Lance -> weapon.numCharges < 7.0 && weaponId !in ship.usedArmaments
@@ -834,7 +878,7 @@ else when (phase) {
                        .filterKeys { canShipAttack(it) }
                        .filterValues { it.owner == forPlayer }
                        .flatMap { (id, ship) ->
-                               ship.armaments.weaponInstances.keys.mapNotNull { weaponId ->
+                               ship.armaments.keys.mapNotNull { weaponId ->
                                        PlayerAbilityType.UseWeapon(id, weaponId).takeIf {
                                                weaponId !in ship.usedArmaments && ship.canUseWeapon(weaponId)
                                        }
@@ -847,10 +891,16 @@ else when (phase) {
                        .keys
                        .map { PlayerAbilityType.DisruptionPulse(it) }
                
+               val usableBoardingTransportaria = ships
+                       .filterKeys { canShipAttack(it) }
+                       .filterValues { it.owner == forPlayer && !it.isDoneCurrentPhase && it.canSendBoardingParty }
+                       .keys
+                       .map { PlayerAbilityType.BoardingParty(it) }
+               
                val recallableStrikeWings = ships
                        .filterValues { it.owner == forPlayer }
                        .flatMap { (id, ship) ->
-                               ship.armaments.weaponInstances.mapNotNull { (weaponId, weapon) ->
+                               ship.armaments.mapNotNull { (weaponId, weapon) ->
                                        PlayerAbilityType.RecallStrikeCraft(id, weaponId).takeIf {
                                                weaponId in ship.usedArmaments && weapon is ShipWeaponInstance.Hangar
                                        }
@@ -861,7 +911,7 @@ else when (phase) {
                        listOf(PlayerAbilityType.DonePhase(GamePhase.Attack(phase.turn)))
                else emptyList()
                
-               chargeableLances + usableWeapons + recallableStrikeWings + usableDisruptionPulses + finishAttacking
+               usableBoardingTransportaria + chargeableLances + usableWeapons + recallableStrikeWings + usableDisruptionPulses + finishAttacking
        }
        is GamePhase.Repair -> {
                val repairableModules = ships
index 74ea87eb1ea47788a7f74db58cfc5a9256b86691..a1b2d688427524739c1ad566572ddbf3b9600b68 100644 (file)
@@ -45,6 +45,15 @@ sealed class ChatEntry {
                val damageIgnoreType: DamageIgnoreType,
        ) : ChatEntry()
        
+       @Serializable
+       data class ShipBoarded(
+               val ship: Id<ShipInstance>,
+               val boarder: Id<ShipInstance>,
+               override val sentAt: Moment,
+               val critical: ShipCritical?,
+               val damageAmount: Int = 0,
+       ) : ChatEntry()
+       
        @Serializable
        data class ShipDestroyed(
                val ship: Id<ShipInstance>,
@@ -73,6 +82,9 @@ sealed class ShipCritical {
        @Serializable
        object Fire : ShipCritical()
        
+       @Serializable
+       data class TroopsKilled(val number: Int) : ShipCritical()
+       
        @Serializable
        data class ModulesHit(val module: Set<ShipModule>) : ShipCritical()
 }
@@ -81,6 +93,7 @@ fun CritResult.report(): ShipCritical? = when (this) {
        CritResult.NoEffect -> null
        is CritResult.FireStarted -> ShipCritical.Fire
        is CritResult.ModulesDisabled -> ShipCritical.ModulesHit(modules)
+       is CritResult.TroopsKilled -> ShipCritical.TroopsKilled(amount)
        is CritResult.HullDamaged -> ShipCritical.ExtraDamage
        is CritResult.Destroyed -> null
 }
index 0a31e5b6e7f6f5c23b3957758cc5e2c133c85a68..3e9b7004305c0d59b5833f28eef83b49d8847097 100644 (file)
@@ -40,7 +40,7 @@ fun GameState.getValidAttackersWith(target: ShipInstance): Map<Id<ShipInstance>,
 }
 
 fun GameState.isValidAttackerWith(attacker: ShipInstance, target: ShipInstance): Set<Id<ShipWeapon>> {
-       return attacker.armaments.weaponInstances.filterValues {
+       return attacker.armaments.filterValues {
                isValidTarget(attacker, it, attacker.getWeaponPickRequest(it.weapon), target)
        }.keys
 }
@@ -77,12 +77,12 @@ fun GameState.calculateAttackPhaseInitiative(): InitiativePair = InitiativePair(
                        shipList
                                .filter { !it.isDoneCurrentPhase }
                                .sumOf { ship ->
-                                       val allWeapons = ship.armaments.weaponInstances
+                                       val allWeapons = ship.armaments
                                                .filterValues { weapon -> hasValidTargets(ship, weapon) }
                                        val usableWeapons = allWeapons - ship.usedArmaments
                                        
-                                       val allWeaponShots = allWeapons.values.sumOf { it.weapon.numShots }
-                                       val usableWeaponShots = usableWeapons.values.sumOf { it.weapon.numShots }
+                                       val allWeaponShots = allWeapons.values.sumOf { it.weapon.numShots } + ship.troopsAmount
+                                       val usableWeaponShots = usableWeapons.values.sumOf { it.weapon.numShots } + (if (ship.canSendBoardingParty) ship.troopsAmount else 0)
                                        
                                        ship.ship.pointCost * (usableWeaponShots.toDouble() / allWeaponShots)
                                }
index eb64f21973098fb3f75ed08d597b53d9a01ae2dc..75585308b6b85044fe5cb540ed157c505e0ecf97 100644 (file)
@@ -2,8 +2,6 @@ package starshipfights.game
 
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
-import kotlin.random.Random
-import kotlin.random.nextInt
 
 @Serializable
 data class GameState(
@@ -99,7 +97,7 @@ private fun GameState.afterPhase(): GameState {
                                if (ship.numFires <= 0)
                                        return@fireDamage id to ship
                                
-                               val hits = Random.nextInt(0..ship.numFires)
+                               val hits = (0..ship.numFires).random()
                                
                                val impactResult = ship.impact(hits, true)
                                newChatEntries += listOfNotNull(impactResult.toChatEntry(ShipAttacker.Fire, null))
@@ -126,6 +124,8 @@ private fun GameState.afterPhase(): GameState {
                                        fighterWings = emptySet(),
                                        bomberWings = emptySet(),
                                        usedArmaments = emptySet(),
+                                       
+                                       hasSentBoardingParty = false,
                                )
                        }
                }
index 6811e5c33f950b1bae029b0d5e608f3187f63881..123b197e263fda5b58780e01dc3cddd0f2051a48 100644 (file)
@@ -142,18 +142,21 @@ val ShipWeightClass.movement: ShipMovement
 sealed class ShipDurability {
        abstract val maxHullPoints: Int
        abstract val turretDefense: Double
+       abstract val troopsDefense: Int
 }
 
 @Serializable
 data class StandardShipDurability(
        override val maxHullPoints: Int,
        override val turretDefense: Double,
+       override val troopsDefense: Int,
        val repairTokens: Int,
 ) : ShipDurability()
 
 @Serializable
 data class FelinaeShipDurability(
        override val maxHullPoints: Int,
+       override val troopsDefense: Int,
        val disruptionPulseRange: Double,
        val disruptionPulseShots: Int
 ) : ShipDurability() {
@@ -163,31 +166,31 @@ data class FelinaeShipDurability(
 
 val ShipWeightClass.durability: ShipDurability
        get() = when (this) {
-               ShipWeightClass.ESCORT -> StandardShipDurability(4, 0.5, 1)
-               ShipWeightClass.DESTROYER -> StandardShipDurability(8, 0.5, 1)
-               ShipWeightClass.CRUISER -> StandardShipDurability(12, 1.0, 2)
-               ShipWeightClass.BATTLECRUISER -> StandardShipDurability(14, 1.5, 2)
-               ShipWeightClass.BATTLESHIP -> StandardShipDurability(16, 2.0, 3)
-               
-               ShipWeightClass.BATTLE_BARGE -> StandardShipDurability(16, 1.5, 3)
-               
-               ShipWeightClass.GRAND_CRUISER -> StandardShipDurability(15, 1.75, 3)
-               ShipWeightClass.COLOSSUS -> StandardShipDurability(27, 3.0, 4)
-               
-               ShipWeightClass.FF_ESCORT -> FelinaeShipDurability(6, 1000.0, 3)
-               ShipWeightClass.FF_DESTROYER -> FelinaeShipDurability(9, 1000.0, 4)
-               ShipWeightClass.FF_CRUISER -> FelinaeShipDurability(12, 750.0, 2)
-               ShipWeightClass.FF_BATTLECRUISER -> FelinaeShipDurability(15, 875.0, 2)
-               ShipWeightClass.FF_BATTLESHIP -> FelinaeShipDurability(18, 1250.0, 3)
-               
-               ShipWeightClass.AUXILIARY_SHIP -> StandardShipDurability(4, 2.0, 1)
-               ShipWeightClass.LIGHT_CRUISER -> StandardShipDurability(8, 3.0, 2)
-               ShipWeightClass.MEDIUM_CRUISER -> StandardShipDurability(12, 3.5, 2)
-               ShipWeightClass.HEAVY_CRUISER -> StandardShipDurability(16, 4.0, 3)
-               
-               ShipWeightClass.FRIGATE -> StandardShipDurability(10, 1.5, 1)
-               ShipWeightClass.LINE_SHIP -> StandardShipDurability(15, 2.0, 1)
-               ShipWeightClass.DREADNOUGHT -> StandardShipDurability(20, 2.5, 1)
+               ShipWeightClass.ESCORT -> StandardShipDurability(4, 0.5, 5, 1)
+               ShipWeightClass.DESTROYER -> StandardShipDurability(8, 0.5, 7, 1)
+               ShipWeightClass.CRUISER -> StandardShipDurability(12, 1.0, 10, 2)
+               ShipWeightClass.BATTLECRUISER -> StandardShipDurability(14, 1.5, 10, 2)
+               ShipWeightClass.BATTLESHIP -> StandardShipDurability(16, 2.0, 15, 3)
+               
+               ShipWeightClass.BATTLE_BARGE -> StandardShipDurability(16, 1.5, 15, 3)
+               
+               ShipWeightClass.GRAND_CRUISER -> StandardShipDurability(15, 1.75, 12, 3)
+               ShipWeightClass.COLOSSUS -> StandardShipDurability(27, 3.0, 25, 4)
+               
+               ShipWeightClass.FF_ESCORT -> FelinaeShipDurability(6, 3, 1000.0, 3)
+               ShipWeightClass.FF_DESTROYER -> FelinaeShipDurability(9, 4, 1000.0, 4)
+               ShipWeightClass.FF_CRUISER -> FelinaeShipDurability(12, 5, 750.0, 2)
+               ShipWeightClass.FF_BATTLECRUISER -> FelinaeShipDurability(15, 6, 875.0, 2)
+               ShipWeightClass.FF_BATTLESHIP -> FelinaeShipDurability(18, 7, 1250.0, 3)
+               
+               ShipWeightClass.AUXILIARY_SHIP -> StandardShipDurability(4, 2.0, 6, 1)
+               ShipWeightClass.LIGHT_CRUISER -> StandardShipDurability(8, 3.0, 9, 2)
+               ShipWeightClass.MEDIUM_CRUISER -> StandardShipDurability(12, 3.5, 12, 2)
+               ShipWeightClass.HEAVY_CRUISER -> StandardShipDurability(16, 4.0, 15, 3)
+               
+               ShipWeightClass.FRIGATE -> StandardShipDurability(10, 1.5, 7, 1)
+               ShipWeightClass.LINE_SHIP -> StandardShipDurability(15, 2.0, 9, 1)
+               ShipWeightClass.DREADNOUGHT -> StandardShipDurability(20, 2.5, 11, 1)
        }
 
 @Serializable
diff --git a/src/commonMain/kotlin/starshipfights/game/ship_boarding.kt b/src/commonMain/kotlin/starshipfights/game/ship_boarding.kt
new file mode 100644 (file)
index 0000000..16c1194
--- /dev/null
@@ -0,0 +1,169 @@
+package starshipfights.game
+
+import starshipfights.data.Id
+import kotlin.math.roundToInt
+
+fun factionBoardingModifier(faction: Faction): Int = when (faction) {
+       Faction.MECHYRDIA -> 7
+       Faction.NDRC -> 10
+       Faction.MASRA_DRAETSEN -> 8
+       Faction.FELINAE_FELICES -> 0
+       Faction.ISARNAREYKK -> 3
+       Faction.VESTIGIUM -> 2
+}
+
+fun weightClassBoardingModifier(weightClass: ShipWeightClass): Int = when (weightClass) {
+       ShipWeightClass.ESCORT -> 2
+       ShipWeightClass.DESTROYER -> 2
+       ShipWeightClass.CRUISER -> 4
+       ShipWeightClass.BATTLECRUISER -> 4
+       ShipWeightClass.BATTLESHIP -> 6
+       
+       ShipWeightClass.BATTLE_BARGE -> 8
+       
+       ShipWeightClass.GRAND_CRUISER -> 6
+       ShipWeightClass.COLOSSUS -> 10
+       
+       ShipWeightClass.FF_ESCORT -> 0
+       ShipWeightClass.FF_DESTROYER -> 2
+       ShipWeightClass.FF_CRUISER -> 2
+       ShipWeightClass.FF_BATTLECRUISER -> 4
+       ShipWeightClass.FF_BATTLESHIP -> 6
+       
+       ShipWeightClass.AUXILIARY_SHIP -> 0
+       ShipWeightClass.LIGHT_CRUISER -> 2
+       ShipWeightClass.MEDIUM_CRUISER -> 4
+       ShipWeightClass.HEAVY_CRUISER -> 6
+       
+       ShipWeightClass.FRIGATE -> 0
+       ShipWeightClass.LINE_SHIP -> 2
+       ShipWeightClass.DREADNOUGHT -> 4
+}
+
+fun troopsBoardingModifier(troopsAmount: Int, totalTroops: Int): Int = when {
+       troopsAmount < totalTroops / 3 -> 0
+       troopsAmount < (totalTroops * 2) / 3 -> 2
+       troopsAmount < totalTroops -> 3
+       troopsAmount == totalTroops -> 4
+       else -> 4
+}
+
+fun hullBoardingModifier(hullAmount: Int, totalHull: Int): Int = when {
+       hullAmount < totalHull / 2 -> 1
+       hullAmount < totalHull -> 3
+       hullAmount == totalHull -> 5
+       else -> 5
+}
+
+fun turretsBoardingModifier(turretsDefense: Double, turretsStatus: ShipModuleStatus): Int = when (turretsStatus) {
+       ShipModuleStatus.INTACT -> turretsDefense.roundToInt()
+       ShipModuleStatus.DAMAGED -> (turretsDefense * 0.5).roundToInt()
+       else -> 0
+}
+
+fun shieldsBoardingAssaultModifier(shieldsAmount: Int, totalShields: Int): Int = when {
+       shieldsAmount == 0 -> 2
+       shieldsAmount < totalShields -> 1
+       else -> 0
+}
+
+fun shieldsBoardingDefenseModifier(shieldsAmount: Int, totalShields: Int): Int = when {
+       shieldsAmount == 0 -> 0
+       shieldsAmount <= totalShields / 2 -> 1
+       shieldsAmount < totalShields -> 2
+       else -> 3
+}
+
+fun assaultBoardingModifier(assaultModuleStatus: ShipModuleStatus): Int = when (assaultModuleStatus) {
+       ShipModuleStatus.INTACT -> 5
+       ShipModuleStatus.DAMAGED -> 3
+       ShipModuleStatus.DESTROYED -> 0
+       else -> 0
+}
+
+fun defenseBoardingModifier(defenseModuleStatus: ShipModuleStatus): Int = when (defenseModuleStatus) {
+       ShipModuleStatus.INTACT -> 3
+       ShipModuleStatus.DAMAGED -> 2
+       ShipModuleStatus.DESTROYED -> 0
+       else -> 0
+}
+
+val ShipInstance.assaultModifier: Int
+       get() = listOf(
+               factionBoardingModifier(ship.shipType.faction),
+               weightClassBoardingModifier(ship.shipType.weightClass),
+               troopsBoardingModifier(troopsAmount, durability.troopsDefense),
+               hullBoardingModifier(hullAmount, durability.maxHullPoints),
+               turretsBoardingModifier(durability.turretDefense, modulesStatus[ShipModule.Turrets]),
+               if (canUseShields)
+                       shieldsBoardingAssaultModifier(shieldAmount, powerMode.shields)
+               else shieldsBoardingAssaultModifier(0, powerMode.shields),
+               assaultBoardingModifier(modulesStatus[ShipModule.Assault]),
+       ).sum()
+
+val ShipInstance.defenseModifier: Int
+       get() = listOf(
+               factionBoardingModifier(ship.shipType.faction),
+               weightClassBoardingModifier(ship.shipType.weightClass),
+               troopsBoardingModifier(troopsAmount, durability.troopsDefense),
+               hullBoardingModifier(hullAmount, durability.maxHullPoints),
+               turretsBoardingModifier(durability.turretDefense, modulesStatus[ShipModule.Turrets]),
+               if (canUseShields)
+                       shieldsBoardingDefenseModifier(shieldAmount, powerMode.shields)
+               else shieldsBoardingDefenseModifier(0, powerMode.shields),
+               defenseBoardingModifier(modulesStatus[ShipModule.Defense]),
+       ).sum()
+
+fun boardingRoll(): Int = (0..8).random() + (0..8).random()
+
+fun ShipInstance.board(defender: ShipInstance): ImpactResult {
+       val myValue = assaultModifier + boardingRoll()
+       val otherValue = defender.defenseModifier + boardingRoll()
+       
+       return when {
+               otherValue * 2 < myValue -> {
+                       when (val firstImpact = ImpactResult.Intact(defender).withCritResult(defender.doCriticalDamage())) {
+                               is ImpactResult.Damaged -> firstImpact.withCritResult(firstImpact.ship.doCriticalDamage())
+                               else -> firstImpact
+                       }
+               }
+               otherValue <= myValue -> {
+                       ImpactResult.Intact(defender).withCritResult(defender.doCriticalDamage())
+               }
+               else -> {
+                       val troopsKilled = (1..(myValue / 2)).randomOrNull() ?: 0
+                       ImpactResult.Intact(defender).withCritResult(defender.killTroops(troopsKilled))
+               }
+       }
+}
+
+fun ShipInstance.afterBoarding() = if (troopsAmount <= 1) null else copy(
+       troopsAmount = troopsAmount - 1,
+       hasSentBoardingParty = true,
+)
+
+fun reportBoardingResult(impactResult: ImpactResult, attacker: Id<ShipInstance>) = when (impactResult) {
+       is ImpactResult.Destroyed -> ChatEntry.ShipDestroyed(
+               ship = impactResult.ship.id,
+               sentAt = Moment.now,
+               destroyedBy = ShipAttacker.EnemyShip(attacker)
+       )
+       is ImpactResult.Damaged -> ChatEntry.ShipBoarded(
+               ship = impactResult.ship.id,
+               boarder = attacker,
+               sentAt = Moment.now,
+               critical = impactResult.critical.report(),
+               damageAmount = impactResult.damage.amount
+       )
+}
+
+fun ShipInstance.getBoardingPickRequest() = PickRequest(
+       PickType.Ship(allowSides = setOf(owner.other)),
+       PickBoundary.WeaponsFire(
+               center = position.location,
+               facing = position.facing,
+               minDistance = SHIP_BASE_SIZE,
+               maxDistance = firepower.rangeMultiplier * SHIP_TRANSPORTARIUM_RANGE,
+               firingArcs = FiringArc.FIRE_FORE_270,
+       )
+)
index 9c2351e518995124842698b9cc46209fce708316..894412767719c51487c4f869a845bfe356896d94 100644 (file)
@@ -21,6 +21,7 @@ data class ShipInstance(
        val weaponAmount: Int = powerMode.weapons,
        val shieldAmount: Int = powerMode.shields,
        val hullAmount: Int = ship.durability.maxHullPoints,
+       val troopsAmount: Int = ship.durability.troopsDefense,
        
        val modulesStatus: ShipModulesStatus = ShipModulesStatus.forShip(ship),
        val numFires: Int = 0,
@@ -38,6 +39,8 @@ data class ShipInstance(
        
        val fighterWings: Set<ShipHangarWing> = emptySet(),
        val bomberWings: Set<ShipHangarWing> = emptySet(),
+       
+       val hasSentBoardingParty: Boolean = false,
 ) {
        val canUseShields: Boolean
                get() = ship.hasShields && modulesStatus[ShipModule.Shields].canBeUsed
@@ -45,8 +48,8 @@ data class ShipInstance(
        val canUseTurrets: Boolean
                get() = modulesStatus[ShipModule.Turrets].canBeUsed
        
-       val canCatchFire: Boolean
-               get() = ship.shipType.faction != Faction.FELINAE_FELICES
+       val canSendBoardingParty: Boolean
+               get() = modulesStatus[ShipModule.Assault].canBeUsed && troopsAmount > 1 && !hasSentBoardingParty
        
        val canUseInertialessDrive: Boolean
                get() = ship.canUseInertialessDrive && modulesStatus[ShipModule.Engines].canBeUsed && when (val movement = ship.movement) {
@@ -82,7 +85,7 @@ data class ShipInstance(
                if (!modulesStatus[ShipModule.Weapon(weaponId)].canBeUsed)
                        return false
                
-               val weapon = armaments.weaponInstances[weaponId] ?: return false
+               val weapon = armaments[weaponId] ?: return false
                
                return when (weapon) {
                        is ShipWeaponInstance.Cannon -> weaponAmount > 0
@@ -264,6 +267,8 @@ else
 
 const val SHIP_BASE_SIZE = 250.0
 
+const val SHIP_TRANSPORTARIUM_RANGE = 1_500.0
+
 const val SHIP_TORPEDO_RANGE = 2_000.0
 const val SHIP_CANNON_RANGE = 2_500.0
 const val SHIP_LANCE_RANGE = 3_000.0
index b3f78b9388a10326d9f4f42c460d7dc7a80dbd41..ded8f80e5755d276ed694397d8f5e3a82a3745d9 100644 (file)
@@ -9,8 +9,6 @@ import kotlinx.serialization.encoding.Decoder
 import kotlinx.serialization.encoding.Encoder
 import starshipfights.data.Id
 import kotlin.jvm.JvmInline
-import kotlin.random.Random
-import kotlin.random.nextInt
 
 @Serializable
 sealed class ShipModule {
@@ -19,7 +17,21 @@ sealed class ShipModule {
        @Serializable
        data class Weapon(val weaponId: Id<ShipWeapon>) : ShipModule() {
                override fun getDisplayName(ship: Ship): String {
-                       return ship.armaments.weapons[weaponId]?.displayName ?: ""
+                       return ship.armaments[weaponId]?.displayName ?: ""
+               }
+       }
+       
+       @Serializable
+       object Assault : ShipModule() {
+               override fun getDisplayName(ship: Ship): String {
+                       return "Boarding Transportarium"
+               }
+       }
+       
+       @Serializable
+       object Defense : ShipModule() {
+               override fun getDisplayName(ship: Ship): String {
+                       return "Internal Defenses"
                }
        }
        
@@ -59,7 +71,7 @@ value class ShipModulesStatus(val statuses: Map<ShipModule, ShipModuleStatus>) {
        operator fun get(module: ShipModule) = statuses[module] ?: ShipModuleStatus.ABSENT
        
        fun repair(module: ShipModule, repairUnrepairable: Boolean = false) = ShipModulesStatus(
-               statuses + if (this[module].canBeRepaired || (repairUnrepairable && !this[module].canBeUsed))
+               statuses + if (this[module].canBeRepaired || (repairUnrepairable && this[module] in ShipModuleStatus.DAMAGED..ShipModuleStatus.DESTROYED))
                        mapOf(module to ShipModuleStatus.values()[this[module].ordinal - 1])
                else emptyMap()
        )
@@ -89,10 +101,12 @@ value class ShipModulesStatus(val statuses: Map<ShipModule, ShipModuleStatus>) {
        companion object {
                fun forShip(ship: Ship) = ShipModulesStatus(
                        mapOf(
+                               ShipModule.Assault to ShipModuleStatus.INTACT,
+                               ShipModule.Defense to ShipModuleStatus.INTACT,
                                ShipModule.Shields to if (ship.hasShields) ShipModuleStatus.INTACT else ShipModuleStatus.ABSENT,
                                ShipModule.Engines to ShipModuleStatus.INTACT,
                                ShipModule.Turrets to ShipModuleStatus.INTACT,
-                       ) + ship.armaments.weapons.keys.associate {
+                       ) + ship.armaments.keys.associate {
                                ShipModule.Weapon(it) to ShipModuleStatus.INTACT
                        }
                )
@@ -118,6 +132,7 @@ sealed class CritResult {
        object NoEffect : CritResult()
        data class FireStarted(val ship: ShipInstance) : CritResult()
        data class ModulesDisabled(val ship: ShipInstance, val modules: Set<ShipModule>) : CritResult()
+       data class TroopsKilled(val ship: ShipInstance, val amount: Int) : CritResult()
        data class HullDamaged(val ship: ShipInstance, val amount: Int) : CritResult()
        data class Destroyed(val ship: ShipWreck) : CritResult()
        
@@ -130,10 +145,10 @@ sealed class CritResult {
 }
 
 fun ShipInstance.doCriticalDamage(): CritResult {
-       if (!canCatchFire)
-               return doCriticalDamageUninflammable()
+       if (ship.shipType.faction == Faction.FELINAE_FELICES)
+               return doCriticalDamageFelinae()
        
-       return when (Random.nextInt(0..6) + Random.nextInt(0..6)) { // Ranges in 0..12, probability density peaks at 6
+       return when ((0..8).random() + (0..8).random()) { // Ranges in 0..16, probability density peaks at 8
                0 -> {
                        // Damage ALL the modules!
                        val modulesDamaged = modulesStatus.statuses.filter { (k, v) -> k !is ShipModule.Weapon && v != ShipModuleStatus.ABSENT }.keys
@@ -144,7 +159,7 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                }
                1 -> {
                        // Damage 3 weapons
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -152,7 +167,7 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                }
                2 -> {
                        // Damage 2 weapons
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(2).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(2).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -168,7 +183,7 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                }
                4 -> {
                        // Damage 1 weapon
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -183,18 +198,31 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                        )
                }
                6 -> {
+                       // Damage transportarium
+                       val moduleDamaged = ShipModule.Assault
+                       CritResult.ModulesDisabled(
+                               copy(modulesStatus = modulesStatus.damage(moduleDamaged)),
+                               setOf(moduleDamaged)
+                       )
+               }
+               7 -> {
+                       // Lose a few troops
+                       val deaths = (1..2).random()
+                       killTroops(deaths)
+               }
+               8 -> {
                        // Fire!
                        CritResult.FireStarted(
                                copy(numFires = numFires + 1)
                        )
                }
-               7 -> {
+               9 -> {
                        // Two fires!
                        CritResult.FireStarted(
                                copy(numFires = numFires + 2)
                        )
                }
-               8 -> {
+               10 -> {
                        // Damage turrets
                        val moduleDamaged = ShipModule.Turrets
                        CritResult.ModulesDisabled(
@@ -202,7 +230,20 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                                setOf(moduleDamaged)
                        )
                }
-               9 -> {
+               11 -> {
+                       // Lose many troops
+                       val deaths = (1..2).random() + (1..2).random()
+                       killTroops(deaths)
+               }
+               12 -> {
+                       // Damage security system
+                       val moduleDamaged = ShipModule.Defense
+                       CritResult.ModulesDisabled(
+                               copy(modulesStatus = modulesStatus.damage(moduleDamaged)),
+                               setOf(moduleDamaged)
+                       )
+               }
+               13 -> {
                        // Damage random module
                        val moduleDamaged = modulesStatus.statuses.keys.filter { it !is ShipModule.Weapon }.random()
                        CritResult.ModulesDisabled(
@@ -210,7 +251,7 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                                setOf(moduleDamaged)
                        )
                }
-               10 -> {
+               14 -> {
                        // Damage shields
                        val moduleDamaged = ShipModule.Shields
                        if (ship.hasShields)
@@ -224,22 +265,22 @@ fun ShipInstance.doCriticalDamage(): CritResult {
                        else
                                CritResult.NoEffect
                }
-               11 -> {
+               15 -> {
                        // Hull breach
-                       val damage = Random.nextInt(0..2) + Random.nextInt(1..3)
-                       CritResult.fromImpactResult(impact(damage))
+                       val damage = (0..2).random() + (1..3).random()
+                       CritResult.fromImpactResult(impact(damage, true))
                }
-               12 -> {
+               16 -> {
                        // Bulkhead collapse
-                       val damage = Random.nextInt(2..4) + Random.nextInt(3..5)
-                       CritResult.fromImpactResult(impact(damage))
+                       val damage = (2..4).random() + (3..5).random()
+                       CritResult.fromImpactResult(impact(damage, true))
                }
                else -> CritResult.NoEffect
        }
 }
 
-private fun ShipInstance.doCriticalDamageUninflammable(): CritResult {
-       return when (Random.nextInt(0..5) + Random.nextInt(0..5)) {
+private fun ShipInstance.doCriticalDamageFelinae(): CritResult {
+       return when ((0..5).random() + (0..5).random()) {
                0 -> {
                        // Damage ALL the modules!
                        val modulesDamaged = modulesStatus.statuses.filter { (k, v) -> k !is ShipModule.Weapon && v != ShipModuleStatus.ABSENT }.keys
@@ -250,7 +291,7 @@ private fun ShipInstance.doCriticalDamageUninflammable(): CritResult {
                }
                1 -> {
                        // Damage 3 weapons
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(3).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -258,7 +299,7 @@ private fun ShipInstance.doCriticalDamageUninflammable(): CritResult {
                }
                2 -> {
                        // Damage 2 weapons
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(2).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(2).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -274,7 +315,7 @@ private fun ShipInstance.doCriticalDamageUninflammable(): CritResult {
                }
                4 -> {
                        // Damage 1 weapon
-                       val modulesDamaged = armaments.weaponInstances.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
+                       val modulesDamaged = armaments.keys.shuffled().take(1).map { ShipModule.Weapon(it) }
                        CritResult.ModulesDisabled(
                                copy(modulesStatus = modulesStatus.damageMany(modulesDamaged)),
                                modulesDamaged.toSet()
@@ -305,27 +346,18 @@ private fun ShipInstance.doCriticalDamageUninflammable(): CritResult {
                        )
                }
                8 -> {
-                       // Damage shields
-                       val moduleDamaged = ShipModule.Shields
-                       if (ship.hasShields)
-                               CritResult.ModulesDisabled(
-                                       copy(
-                                               shieldAmount = 0,
-                                               modulesStatus = modulesStatus.damage(moduleDamaged)
-                                       ),
-                                       setOf(moduleDamaged)
-                               )
-                       else
-                               CritResult.NoEffect
+                       // Lose some troops
+                       val deaths = (1..3).random()
+                       killTroops(deaths)
                }
                9 -> {
                        // Hull breach
-                       val damage = Random.nextInt(0..2) + Random.nextInt(1..3)
+                       val damage = (0..2).random() + (1..3).random()
                        CritResult.fromImpactResult(impact(damage))
                }
                10 -> {
                        // Bulkhead collapse
-                       val damage = Random.nextInt(2..4) + Random.nextInt(3..5)
+                       val damage = (2..4).random() + (3..5).random()
                        CritResult.fromImpactResult(impact(damage))
                }
                else -> CritResult.NoEffect
index 4dc941b8c8de316f05db4a210d4f0e06200365b3..dace42ad3f97df20138214fba93d7881b12cc4b3 100644 (file)
@@ -200,7 +200,7 @@ enum class ShipType(
 }
 
 val ShipType.pointCost: Int
-       get() = weightClass.basePointCost + armaments.weapons.values.sumOf { it.addsPointCost }
+       get() = weightClass.basePointCost + armaments.values.sumOf { it.addsPointCost }
 
 val ShipType.meshName: String
        get() = "${faction.meshTag}-${weightClass.meshIndex}-${toUrlSlug()}-class"
index 97407c28cc9cc7706ed9159cdd7d5f2f85b2cc39..73cf8f9eaa95bd92e874e235aaca35385fe453bf 100644 (file)
@@ -2,7 +2,6 @@ package starshipfights.game
 
 import kotlinx.serialization.Serializable
 import starshipfights.data.Id
-import kotlin.jvm.JvmInline
 import kotlin.math.*
 import kotlin.random.Random
 
@@ -282,19 +281,11 @@ sealed class ShipWeaponInstance {
        }
 }
 
-@JvmInline
-@Serializable
-value class ShipArmaments(
-       val weapons: Map<Id<ShipWeapon>, ShipWeapon>
-) {
-       fun instantiate() = ShipInstanceArmaments(weapons.mapValues { (_, weapon) -> weapon.instantiate() })
-}
+typealias ShipArmaments = Map<Id<ShipWeapon>, ShipWeapon>
 
-@JvmInline
-@Serializable
-value class ShipInstanceArmaments(
-       val weaponInstances: Map<Id<ShipWeapon>, ShipWeaponInstance>
-)
+fun ShipArmaments.instantiate() = mapValues { (_, weapon) -> weapon.instantiate() }
+
+typealias ShipInstanceArmaments = Map<Id<ShipWeapon>, ShipWeaponInstance>
 
 fun cannonChanceToHit(attacker: ShipInstance, targeted: ShipInstance): Double {
        val relativeDistance = attacker.position.location - targeted.position.location
@@ -349,6 +340,10 @@ fun ShipInstance.felinaeArmorIgnoreDamageChance(): Double {
        return -expm1(-exponent)
 }
 
+fun ShipInstance.killTroops(damage: Int) = if (damage >= troopsAmount)
+       CritResult.Destroyed(ShipWreck(ship, owner))
+else CritResult.TroopsKilled(copy(troopsAmount = troopsAmount - damage), damage)
+
 fun ShipInstance.impact(damage: Int, ignoreShields: Boolean = false) = if (durability is FelinaeShipDurability && Random.nextDouble() < felinaeArmorIgnoreDamageChance())
        ImpactResult.Intact(this, DamageIgnoreType.FELINAE_ARMOR)
 else if (ignoreShields) {
@@ -367,42 +362,42 @@ data class ShipHangarWing(
        val hangar: Id<ShipWeapon>
 )
 
-fun ShipInstance.afterUsing(weaponId: Id<ShipWeapon>) = when (val weapon = armaments.weaponInstances.getValue(weaponId)) {
+fun ShipInstance.afterUsing(weaponId: Id<ShipWeapon>) = when (val weapon = armaments.getValue(weaponId)) {
        is ShipWeaponInstance.Cannon -> {
                copy(weaponAmount = weaponAmount - 1, usedArmaments = usedArmaments + setOf(weaponId))
        }
        is ShipWeaponInstance.Lance -> {
-               val newWeapons = armaments.weaponInstances + mapOf(
+               val newWeapons = armaments + mapOf(
                        weaponId to weapon.copy(numCharges = 0.0)
                )
                
-               copy(armaments = ShipInstanceArmaments(newWeapons), usedArmaments = usedArmaments + setOf(weaponId))
+               copy(armaments = newWeapons, usedArmaments = usedArmaments + setOf(weaponId))
        }
        is ShipWeaponInstance.MegaCannon -> {
-               val newWeapons = armaments.weaponInstances + mapOf(
+               val newWeapons = armaments + mapOf(
                        weaponId to weapon.copy(remainingShots = weapon.remainingShots - 1)
                )
                
-               copy(armaments = ShipInstanceArmaments(newWeapons), usedArmaments = usedArmaments + setOf(weaponId))
+               copy(armaments = newWeapons, usedArmaments = usedArmaments + setOf(weaponId))
        }
        is ShipWeaponInstance.RevelationGun -> {
-               val newWeapons = armaments.weaponInstances + mapOf(
+               val newWeapons = armaments + mapOf(
                        weaponId to weapon.copy(remainingShots = weapon.remainingShots - 1)
                )
                
-               copy(armaments = ShipInstanceArmaments(newWeapons), usedArmaments = usedArmaments + setOf(weaponId))
+               copy(armaments = newWeapons, usedArmaments = usedArmaments + setOf(weaponId))
        }
        is ShipWeaponInstance.EmpAntenna -> {
-               val newWeapons = armaments.weaponInstances + mapOf(
+               val newWeapons = armaments + mapOf(
                        weaponId to weapon.copy(remainingShots = weapon.remainingShots - 1)
                )
                
-               copy(armaments = ShipInstanceArmaments(newWeapons), usedArmaments = usedArmaments + setOf(weaponId))
+               copy(armaments = newWeapons, usedArmaments = usedArmaments + setOf(weaponId))
        }
        else -> copy(usedArmaments = usedArmaments + setOf(weaponId))
 }
 
-fun ShipInstance.afterTargeted(by: ShipInstance, weaponId: Id<ShipWeapon>) = when (val weapon = by.armaments.weaponInstances.getValue(weaponId)) {
+fun ShipInstance.afterTargeted(by: ShipInstance, weaponId: Id<ShipWeapon>) = when (val weapon = by.armaments.getValue(weaponId)) {
        is ShipWeaponInstance.Cannon -> {
                var hits = 0
                
@@ -479,11 +474,11 @@ fun ShipInstance.calculateBombing(otherShips: Map<Id<ShipInstance>, ShipInstance
                return null
        
        val totalFighterHealth = fighterWings.sumOf { (carrierId, wingId) ->
-               (otherShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+               (otherShips[carrierId]?.armaments?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
        } + durability.turretDefense + extraFighters
        
        val totalBomberHealth = bomberWings.sumOf { (carrierId, wingId) ->
-               (otherShips[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+               (otherShips[carrierId]?.armaments?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
        } + extraBombers
        
        if (totalBomberHealth < EPSILON)
@@ -514,13 +509,13 @@ fun ShipInstance.afterBombed(otherShips: Map<Id<ShipInstance>, ShipInstance>, st
 }
 
 fun ShipInstance.afterBombing(strikeWingDamage: Map<ShipHangarWing, Double>): ShipInstance {
-       val newArmaments = armaments.weaponInstances.mapValues { (weaponId, weapon) ->
+       val newArmaments = armaments.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))
+       return copy(armaments = newArmaments)
 }
 
 fun ImpactResult.Damaged.withCritResult(critical: CritResult): ImpactResult = when (critical) {
@@ -535,6 +530,11 @@ fun ImpactResult.Damaged.withCritResult(critical: CritResult): ImpactResult = wh
                damage = damage,
                critical = critical
        )
+       is CritResult.TroopsKilled -> copy(
+               ship = critical.ship,
+               damage = damage,
+               critical = critical
+       )
        is CritResult.HullDamaged -> copy(
                ship = critical.ship,
                damage = damage + critical.amount,
@@ -573,7 +573,7 @@ fun ImpactResult.applyStrikeCraftCriticals(criticalChance: Double): ImpactResult
 
 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
+       val weapon = attacker.armaments[weaponId] ?: return 0.0
        
        return when (weapon) {
                is ShipWeaponInstance.Torpedo -> if (targetHasShields) 0.0 else 0.375
@@ -635,33 +635,33 @@ fun ShipInstance.getWeaponPickRequest(weapon: ShipWeapon): PickRequest = when (w
 fun ImpactResult.toChatEntry(attacker: ShipAttacker, weapon: ShipWeaponInstance?) = when (this) {
        is ImpactResult.Damaged -> when (damage) {
                is ImpactDamage.Success -> ChatEntry.ShipAttacked(
-                       ship.id,
-                       attacker,
-                       Moment.now,
-                       damage.amount,
-                       weapon?.weapon,
-                       critical.report(),
+                       ship = ship.id,
+                       attacker = attacker,
+                       sentAt = Moment.now,
+                       damageInflicted = damage.amount,
+                       weapon = weapon?.weapon,
+                       critical = critical.report(),
                )
                is ImpactDamage.Failed -> ChatEntry.ShipAttackFailed(
-                       ship.id,
-                       attacker,
-                       Moment.now,
-                       weapon?.weapon,
-                       damage.ignore
+                       ship = ship.id,
+                       attacker = attacker,
+                       sentAt = Moment.now,
+                       weapon = weapon?.weapon,
+                       damageIgnoreType = damage.ignore
                )
                else -> null
        }
        is ImpactResult.Destroyed -> {
                ChatEntry.ShipDestroyed(
-                       ship.id,
-                       Moment.now,
-                       attacker,
+                       ship = ship.id,
+                       sentAt = Moment.now,
+                       destroyedBy = attacker,
                )
        }
 }
 
 fun GameState.useWeaponPickResponse(attacker: ShipInstance, weaponId: Id<ShipWeapon>, target: PickResponse): GameEvent {
-       val weapon = attacker.armaments.weaponInstances[weaponId] ?: return GameEvent.InvalidAction("That weapon does not exist")
+       val weapon = attacker.armaments[weaponId] ?: return GameEvent.InvalidAction("That weapon does not exist")
        
        return when (val weaponType = weapon.weapon) {
                is AreaWeapon -> {
index 989e920c2fad55dce44c118678bb4cdc210222c7..4218eb3d356b89ab3bc03eeeeb1e3db210446c85 100644 (file)
@@ -68,7 +68,7 @@ fun mechyrdiaShipWeapons(
                idCounter.add(weapons, ShipWeapon.Lance(1, FiringArc.FIRE_BROADSIDE, "Dorsal lance turrets"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun mechyrdiaNanoClassWeapons(): ShipArmaments {
@@ -77,7 +77,7 @@ fun mechyrdiaNanoClassWeapons(): ShipArmaments {
        
        idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_FORE_270, "Dorsal lance turrets"))
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun mechyrdiaPicoClassWeapons(): ShipArmaments {
@@ -87,7 +87,7 @@ fun mechyrdiaPicoClassWeapons(): ShipArmaments {
        idCounter.add(weapons, ShipWeapon.Cannon(2, FiringArc.FIRE_FORE_270, "Double-barrel cannon turret"))
        idCounter.add(weapons, ShipWeapon.Torpedo(setOf(FiringArc.BOW), "Fore torpedo launcher"))
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun ndrcShipWeapons(
@@ -123,7 +123,7 @@ fun ndrcShipWeapons(
                idCounter.add(weapons, ShipWeapon.Lance(numBroadsideLances, setOf(FiringArc.ABEAM_STARBOARD), "Starboard lance battery"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun diadochiShipWeapons(
@@ -166,7 +166,7 @@ fun diadochiShipWeapons(
                idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_FORE_270, "Dorsal lance batteries"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun felinaeShipWeapons(
@@ -185,7 +185,7 @@ fun felinaeShipWeapons(
                idCounter.add(weapons, ShipWeapon.LightningYarn(num, arcs, "$displayName lightning yarn"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun fulkreykkShipWeapons(
@@ -214,7 +214,7 @@ fun fulkreykkShipWeapons(
                idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_BROADSIDE, "Broadside lance battery"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
 
 fun vestigiumShipWeapons(
@@ -242,5 +242,5 @@ fun vestigiumShipWeapons(
                        idCounter.add(weapons, ShipWeapon.Hangar(StrikeCraftWing.BOMBERS, "Bomber complement"))
        }
        
-       return ShipArmaments(weapons)
+       return weapons
 }
index 0c6d3193923ff2a124171f58581c771f242e0c47..8b1be0a4f93c8c570e28d2a3e8ef080b24b0de18 100644 (file)
@@ -271,6 +271,7 @@ object GameUI {
                                                        
                                                        +when (entry.critical) {
                                                                ShipCritical.Fire -> ", starting a fire"
+                                                               is ShipCritical.TroopsKilled -> ", killing ${entry.critical.number} troops"
                                                                is ShipCritical.ModulesHit -> ", disabling ${entry.critical.module.joinToDisplayString { it.getDisplayName(ship) }}"
                                                                else -> ""
                                                        }
@@ -328,6 +329,33 @@ object GameUI {
                                                        }
                                                        +"."
                                                }
+                                               is ChatEntry.ShipBoarded -> {
+                                                       val ship = state.getShipInfo(entry.ship)
+                                                       val owner = state.getShipOwner(entry.ship).relativeTo(mySide)
+                                                       +if (owner == LocalSide.RED)
+                                                               "The enemy ship "
+                                                       else
+                                                               "Our ship, the "
+                                                       strong {
+                                                               style = "color:${owner.htmlColor}"
+                                                               +ship.fullName
+                                                       }
+                                                       
+                                                       +" has been boarded by the "
+                                                       strong {
+                                                               style = "color:${owner.other.htmlColor}"
+                                                               +state.getShipInfo(entry.boarder).fullName
+                                                       }
+                                                       
+                                                       +when (entry.critical) {
+                                                               ShipCritical.ExtraDamage -> ", dealing ${entry.damageAmount} hull damage"
+                                                               ShipCritical.Fire -> ", starting a fire"
+                                                               is ShipCritical.TroopsKilled -> ", killing ${entry.critical.number} troops"
+                                                               is ShipCritical.ModulesHit -> ", disabling ${entry.critical.module.joinToDisplayString { it.getDisplayName(ship) }}"
+                                                               else -> ", to no effect"
+                                                       }
+                                                       +"."
+                                               }
                                                is ChatEntry.ShipDestroyed -> {
                                                        val ship = state.getShipInfo(entry.ship)
                                                        val owner = state.getShipOwner(entry.ship).relativeTo(mySide)
@@ -488,7 +516,7 @@ object GameUI {
                                                val downShield = totalShield - activeShield
                                                
                                                table {
-                                                       style = "width:100%;table-layout:fixed;background-color:#555;margin:0;margin-bottom:25px"
+                                                       style = "width:100%;table-layout:fixed;background-color:#555;margin:0;margin-bottom:10px"
                                                        
                                                        tr {
                                                                repeat(activeShield) {
@@ -510,7 +538,7 @@ object GameUI {
                                        val downHull = totalHull - activeHull
                                        
                                        table {
-                                               style = "width:100%;table-layout:fixed;background-color:#555;margin:0;margin-bottom:25px"
+                                               style = "width:100%;table-layout:fixed;background-color:#555;margin:0;margin-bottom:10px"
                                                
                                                tr {
                                                        repeat(activeHull) {
@@ -526,6 +554,27 @@ object GameUI {
                                                }
                                        }
                                        
+                                       val totalTroops = ship.durability.troopsDefense
+                                       val activeTroops = ship.troopsAmount
+                                       val downTroops = totalTroops - activeTroops
+                                       
+                                       table {
+                                               style = "width:100%;table-layout:fixed;background-color:#555;margin:0;margin-bottom:10px"
+                                               
+                                               tr {
+                                                       repeat(activeTroops) {
+                                                               td {
+                                                                       style = "background-color:#AAA;height:15px;box-shadow:inset 0 0 0 3px #555"
+                                                               }
+                                                       }
+                                                       repeat(downTroops) {
+                                                               td {
+                                                                       style = "background-color:#444;height:15px;box-shadow:inset 0 0 0 3px #555"
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       
                                        if (ship.ship.reactor is StandardShipReactor) {
                                                if (ship.owner == mySide) {
                                                        val totalWeapons = ship.powerMode.weapons
@@ -578,7 +627,7 @@ object GameUI {
                                                +Entities.nbsp
                                                
                                                +ship.fighterWings.sumOf { (carrierId, wingId) ->
-                                                       (state.ships[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+                                                       (state.ships[carrierId]?.armaments?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
                                                }.toPercent()
                                        }
                                }
@@ -601,7 +650,7 @@ object GameUI {
                                                +Entities.nbsp
                                                
                                                +ship.bomberWings.sumOf { (carrierId, wingId) ->
-                                                       (state.ships[carrierId]?.armaments?.weaponInstances?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
+                                                       (state.ships[carrierId]?.armaments?.get(wingId) as? ShipWeaponInstance.Hangar)?.wingHealth ?: 0.0
                                                }.toPercent()
                                        }
                                }
@@ -908,6 +957,16 @@ object GameUI {
                                                                        }
                                                                        br
                                                                }
+                                                               is PlayerAbilityType.BoardingParty -> {
+                                                                       a(href = "#") {
+                                                                               +"Board Enemy Vessel"
+                                                                               onClickFunction = { e ->
+                                                                                       e.preventDefault()
+                                                                                       responder.useAbility(ability)
+                                                                               }
+                                                                       }
+                                                                       br
+                                                               }
                                                                is PlayerAbilityType.RepairShipModule -> {
                                                                        a(href = "#") {
                                                                                +"Repair ${ability.module.getDisplayName(ship.ship)}"
@@ -944,7 +1003,7 @@ object GameUI {
                                                for (ability in combatAbilities) {
                                                        br
                                                        
-                                                       val weaponInstance = ship.armaments.weaponInstances.getValue(ability.weapon)
+                                                       val weaponInstance = ship.armaments.getValue(ability.weapon)
                                                        
                                                        val weaponVerb = if (weaponInstance is ShipWeaponInstance.Hangar) "Release" else "Fire"
                                                        val weaponDesc = weaponInstance.displayName
diff --git a/src/jsMain/resources/images/assault-action.svg b/src/jsMain/resources/images/assault-action.svg
new file mode 100644 (file)
index 0000000..1330c6f
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+               xmlns="http://www.w3.org/2000/svg"
+               width="32px"
+               height="32px"
+               viewBox="0 0 64 64"
+               version="1.1">
+       <path
+                       d="m 1e-5,1.1e-5 9.2444,17.77773 31.28906,31.28854 -0.92088,0.921139 c -1.50815,-0.639349 -3.25327,-0.300419 -4.41239,0.85705 -1.57096,1.57095 -1.57096,4.11785 0,5.6888 1.57094,1.57096 4.11785,1.57096 5.6888,0 1.15769,-1.15912 1.49657,-2.9044 0.85705,-4.41265 l 2.698549,-2.698539 11.02207,9.725759 V 64 h 8.53333 v -8.53333 h -4.85242 l -9.72525,-11.022329 2.69855,-2.69855 c 1.508171,0.63948 3.253451,0.3007 4.41265,-0.85679 1.57096,-1.57095 1.57096,-4.11811 0,-5.68906 -1.570959,-1.57096 -4.11811,-1.57096 -5.68907,0 -1.15764,1.15907 -1.49669,2.90441 -0.8573,4.41265 l -0.92036,0.92062 -31.289059,-31.2888 z m 63.999989,0 -17.77773,9.24465 -12.799999,12.80001 3.5556,3.55559 11.377859,-11.3776 1.42213,1.42213 L 38.4,27.022401 l 3.5556,3.5556 12.8,-12.8 z m -48.355459,14.22213 28.44453,28.44453 -1.42214,1.42213 -28.44453,-28.44452 z m 6.40038,19.20013 -7.11119,7.1112 -0.92088,-0.92088 c 0.63935,-1.50815 0.30042,-3.25327 -0.85705,-4.41239 -1.57095,-1.57096 -4.11812,-1.57096 -5.68906,0 -1.57096,1.57094 -1.57096,4.11785 0,5.6888 1.15912,1.15769 2.90466,1.49683 4.41291,0.85731 l 2.69828,2.69829 -9.7255,11.022069 H 2.6e-4 L 0,64 h 8.53359 l -2.6e-4,-4.85216 11.02233,-9.725509 2.69854,2.698549 c -0.63948,1.50816 -0.30043,3.25345 0.85705,4.41265 1.57095,1.57096 4.11786,1.57095 5.68881,0 1.57095,-1.57095 1.57096,-4.11785 0,-5.6888 -1.15908,-1.157649 -2.90441,-1.496699 -4.41266,-0.85731 l -0.92061,-0.920619 7.11145,-7.1112 v -2.6e-4 l -3.5556,-3.55534 -5.68906,5.68906 -1.42213,-1.42214 5.68906,-5.68906 z m -11.73365,2.35955 c 0.57908,0 1.15807,0.22092 1.5999,0.66275 0.88365,0.88365 0.88365,2.31641 0,3.20006 -0.88365,0.88365 -2.31641,0.88365 -3.20006,0 -0.88365,-0.88365 -0.88365,-2.31641 0,-3.20006 0.44182,-0.44183 1.02107,-0.66275 1.60016,-0.66275 z m 43.37773,0 c 0.57908,0 1.15808,0.22092 1.5999,0.66275 0.883659,0.88365 0.883659,2.31615 0,3.1998 -0.88365,0.88364 -2.316151,0.88365 -3.199811,0 -0.88364,-0.88365 -0.88364,-2.31615 0,-3.1998 0.44183,-0.44183 1.02083,-0.66275 1.599911,-0.66275 z M 38.04447,51.42609 c 0.57908,0 1.15833,0.22093 1.60016,0.66275 0.88365,0.88365 0.88365,2.31641 0,3.20006 -0.88365,0.88365 -2.31641,0.88365 -3.20006,0 -0.88365,-0.88365 -0.88365,-2.31641 0,-3.20006 0.44183,-0.44182 1.02082,-0.66275 1.5999,-0.66275 z m -12.08893,2.6e-4 c 0.57908,0 1.15833,0.22093 1.60016,0.66275 0.88365,0.88366 0.88365,2.31641 0,3.20006 -0.88365,0.88365 -2.31641,0.88365 -3.20006,0 -0.88365,-0.88365 -0.88365,-2.3164 0,-3.20006 0.44182,-0.44182 1.02082,-0.66275 1.5999,-0.66275 z M 6.75567,57.24434 v 4.97799 H 1.77793 v -4.97774 z m 50.488669,0 h 4.97773 v 4.97799 l -4.97747,-2.6e-4 z"
+                       style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:15.00000191;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal"/>
+</svg>
diff --git a/src/jsMain/resources/images/defense-action.svg b/src/jsMain/resources/images/defense-action.svg
new file mode 100644 (file)
index 0000000..784bd75
--- /dev/null
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+               xmlns="http://www.w3.org/2000/svg"
+               width="32px"
+               height="32px"
+               viewBox="0 0 64 64"
+               version="1.1">
+       <path
+                       d="M 32,5.2e-4 A 63.999771,89.568683 0 0 1 0,12.00025 63.999765,60.044211 0 0 0 32,64 63.999765,60.044211 0 0 0 64,12.00025 63.999771,89.568683 0 0 1 32,5.2e-4 Z m 5.3e-4,9.6836 V 54.57684 C 19.11508,46.21992 10.69342,33.41897 8.56327,19.20754 16.80412,17.87434 24.70665,14.55978 32.00052,9.68412 Z"
+                       style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:120.94488525;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:8;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"/>
+</svg>
index 87535493d5f387867f79fcbd4b09b534e00452d8..3d875602a933a628c5fe99727ffefd86224ce572 100644 (file)
@@ -164,7 +164,7 @@ suspend fun ApplicationCall.shipPage(shipType: ShipType): HTML.() -> Unit = page
                                th { +"Firepower" }
                        }
                        
-                       for ((label, weapons) in shipType.armaments.weapons.values.groupBy { it.groupLabel }) {
+                       for ((label, weapons) in shipType.armaments.values.groupBy { it.groupLabel }) {
                                val weapon = weapons.distinct().single()
                                val numShots = weapons.sumOf { it.numShots }