--- /dev/null
+<?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>
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)
}
launch(onGameEnd) {
- for ((phase, canAct) in phasePipe) {
- if (!canAct) continue
+ loop@ for ((phase, canAct) in phasePipe) {
+ if (!canAct) continue@loop
val state = gameState.value
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)
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 ->
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 ->
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)))
}
}
}
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))
}
}
}
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)
}
}
}
+
+fun ShipInstance.expectedBoardingSuccess(against: ShipInstance): Double {
+ return smoothNegative((assaultModifier - against.defenseModifier).toDouble())
+}
}
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()
}
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
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
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(
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)
)
)
)
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))
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
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
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)
bomberWings = targetShip.bomberWings - hangarWing,
)
} + mapOf(ship to newShip)
- )
+ ).withRecalculatedInitiative { calculateAttackPhaseInitiative() }
)
}
}
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(
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() }
)
}
}
@Serializable
object DisruptionPulse : PlayerAbilityData()
+ @Serializable
+ data class BoardingParty(val target: Id<ShipInstance>) : PlayerAbilityData()
+
@Serializable
object RepairShipModule : PlayerAbilityData()
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
.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)
}
.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
}
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
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>,
@Serializable
object Fire : ShipCritical()
+ @Serializable
+ data class TroopsKilled(val number: Int) : ShipCritical()
+
@Serializable
data class ModulesHit(val module: Set<ShipModule>) : ShipCritical()
}
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
}
}
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
}
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)
}
import kotlinx.serialization.Serializable
import starshipfights.data.Id
-import kotlin.random.Random
-import kotlin.random.nextInt
@Serializable
data class 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))
fighterWings = emptySet(),
bomberWings = emptySet(),
usedArmaments = emptySet(),
+
+ hasSentBoardingParty = false,
)
}
}
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() {
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
--- /dev/null
+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,
+ )
+)
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,
val fighterWings: Set<ShipHangarWing> = emptySet(),
val bomberWings: Set<ShipHangarWing> = emptySet(),
+
+ val hasSentBoardingParty: Boolean = false,
) {
val canUseShields: Boolean
get() = ship.hasShields && modulesStatus[ShipModule.Shields].canBeUsed
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) {
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
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
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 {
@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"
}
}
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()
)
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
}
)
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()
}
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
}
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()
}
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()
}
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()
)
}
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(
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(
setOf(moduleDamaged)
)
}
- 10 -> {
+ 14 -> {
// Damage shields
val moduleDamaged = ShipModule.Shields
if (ship.hasShields)
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
}
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()
}
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()
}
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()
)
}
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
}
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"
import kotlinx.serialization.Serializable
import starshipfights.data.Id
-import kotlin.jvm.JvmInline
import kotlin.math.*
import kotlin.random.Random
}
}
-@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
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) {
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
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)
}
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) {
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,
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
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 -> {
idCounter.add(weapons, ShipWeapon.Lance(1, FiringArc.FIRE_BROADSIDE, "Dorsal lance turrets"))
}
- return ShipArmaments(weapons)
+ return weapons
}
fun mechyrdiaNanoClassWeapons(): ShipArmaments {
idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_FORE_270, "Dorsal lance turrets"))
- return ShipArmaments(weapons)
+ return weapons
}
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(
idCounter.add(weapons, ShipWeapon.Lance(numBroadsideLances, setOf(FiringArc.ABEAM_STARBOARD), "Starboard lance battery"))
}
- return ShipArmaments(weapons)
+ return weapons
}
fun diadochiShipWeapons(
idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_FORE_270, "Dorsal lance batteries"))
}
- return ShipArmaments(weapons)
+ return weapons
}
fun felinaeShipWeapons(
idCounter.add(weapons, ShipWeapon.LightningYarn(num, arcs, "$displayName lightning yarn"))
}
- return ShipArmaments(weapons)
+ return weapons
}
fun fulkreykkShipWeapons(
idCounter.add(weapons, ShipWeapon.Lance(2, FiringArc.FIRE_BROADSIDE, "Broadside lance battery"))
}
- return ShipArmaments(weapons)
+ return weapons
}
fun vestigiumShipWeapons(
idCounter.add(weapons, ShipWeapon.Hangar(StrikeCraftWing.BOMBERS, "Bomber complement"))
}
- return ShipArmaments(weapons)
+ return weapons
}
+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 -> ""
}
}
+"."
}
+ 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)
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) {
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) {
}
}
+ 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
+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()
}
}
+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()
}
}
}
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)}"
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
--- /dev/null
+<?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>
--- /dev/null
+<?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>
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 }