Skip to content

Commit

Permalink
Merge pull request #5867 from Algebro7/atmospheric-control-roll-errata
Browse files Browse the repository at this point in the history
Advanced Atmospheric Control Roll Errata
  • Loading branch information
Sleet01 authored Sep 4, 2024
2 parents 404d8d3 + 07bce77 commit 876cd55
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 20 deletions.
4 changes: 3 additions & 1 deletion megamek/i18n/megamek/common/options/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ GameOptionsInfo.option.advanced_movement.description=Newtonian physics-style mov
GameOptionsInfo.option.heat_by_bay.displayableName=StratOps Advanced Heat
GameOptionsInfo.option.heat_by_bay.description=Heat is produced by individual weapons bays rather than arcs
GameOptionsInfo.option.atmospheric_control.displayableName=StratOps Advanced Atmospheric Control Rolls
GameOptionsInfo.option.atmospheric_control.description=atmospheric control rolls only made for damage above the damage threshold
GameOptionsInfo.option.atmospheric_control.description=Atmospheric control rolls occur where the total damage taken by a unit in a turn exceeds its highest damage threshold, or where one damage grouping exceeds the threshold of the target location.
GameOptionsInfo.option.ammo_explosions.displayableName=StratOps Ammo explosions
GameOptionsInfo.option.ammo_explosions.description=Roll 2D6 on critical hits to ammo-fed weapons. On a roll of 10 or higher, the ammunition explodes.
GameOptionsInfo.option.stratops_aa_fire.displayableName=(Unofficial) Advanced Anti-Aircraft
Expand Down Expand Up @@ -489,6 +489,8 @@ GameOptionsInfo.option.crashed_dropships_survive.displayableName=(Unofficial) Un
GameOptionsInfo.option.crashed_dropships_survive.description=If checked, DropShips (and larger) that crash while out of control are not instantly destroyed.
GameOptionsInfo.option.expanded_kf_drive_damage.displayableName=(Unofficial) Damage individual K-F Drive components on K-F Drive Critical Hit
GameOptionsInfo.option.expanded_kf_drive_damage.description=If a K-F Drive critical hit is taken, a random component is hit (per BattleSpace rules).
GameOptionsInfo.option.unoff_adv_atmospheric_control.displayableName=(Unofficial) Old StratOps Advanced Atmospheric Control Rolls
GameOptionsInfo.option.unoff_adv_atmospheric_control.description=Atmospheric control rolls only apply to hit damage above the damage threshold
GameOptionsInfo.group.initiative.displayableName=Initiative Rules
Expand Down
11 changes: 11 additions & 0 deletions megamek/src/megamek/common/Aero.java
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,17 @@ public int getThresh(int loc) {
return 0;
}

@Override
public int getHighestThresh() {
int max = damThresh[0];
for (int i = 1; i < damThresh.length; i++) {
if (damThresh[i] > max) {
max = damThresh[i];
}
}
return max;
}

/**
* Determine if the unit can be repaired, or only harvested for spares.
*
Expand Down
2 changes: 2 additions & 0 deletions megamek/src/megamek/common/IAero.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ default int getClusterMods() {

int getThresh(int loc);

int getHighestThresh();

boolean wasCritThresh();

void setCritThresh(boolean b);
Expand Down
14 changes: 14 additions & 0 deletions megamek/src/megamek/common/LandAirMech.java
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,20 @@ public int getThresh(int loc) {
return getInternal(loc);
}

/**
* @return the highest damage threshold for the LAM unit
*/
@Override
public int getHighestThresh() {
int max = getThresh(0);
for (int i = 1; i < locations(); i++) {
if (getThresh(i) > max) {
max = getThresh(i);
}
}
return max;
}

@Override
public boolean wasCritThresh() {
return critThresh;
Expand Down
1 change: 1 addition & 0 deletions megamek/src/megamek/common/options/GameOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ public synchronized void initialize() {
addOption(advAeroRules, OptionsConstants.ADVAERORULES_AERO_ARTILLERY_MUNITIONS, false);
addOption(advAeroRules, OptionsConstants.ADVAERORULES_CRASHED_DROPSHIPS_SURVIVE, false);
addOption(advAeroRules, OptionsConstants.ADVAERORULES_EXPANDED_KF_DRIVE_DAMAGE, false);
addOption(advAeroRules, OptionsConstants.UNOFF_ADV_ATMOSPHERIC_CONTROL, false);

IBasicOptionGroup initiative = addGroup("initiative");
addOption(initiative, OptionsConstants.INIT_INF_MOVE_EVEN, false);
Expand Down
1 change: 1 addition & 0 deletions megamek/src/megamek/common/options/OptionsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ public class OptionsConstants {
public static final String UNOFF_BLIND_FIGHTER = "blind_fighter";
public static final String UNOFF_SENSOR_GEEK = "sensor_geek";
public static final String UNOFF_SMALL_PILOT = "small_pilot";
public static final String UNOFF_ADV_ATMOSPHERIC_CONTROL = "unoff_adv_atmospheric_control";

// EDGE
public static final String EDGE = "edge";
Expand Down
103 changes: 84 additions & 19 deletions megamek/src/megamek/server/totalwarfare/TWGameManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Manages the Game and processes player actions.
Expand Down Expand Up @@ -16057,9 +16058,10 @@ void checkForPSRFromDamage() {
}
}
if (entity.isAero() && entity.isAirborne() && !game.getBoard().inSpace()) {
// if this aero has any damage, add another roll to the list.
// check if aero unit meets conditions for control rolls and add to the list
if (entity.damageThisPhase > 0) {
if (!getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_ATMOSPHERIC_CONTROL)) {
if (!getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_ATMOSPHERIC_CONTROL)
&& !getGame().getOptions().booleanOption(OptionsConstants.UNOFF_ADV_ATMOSPHERIC_CONTROL)) {
int damMod = entity.damageThisPhase / 20;
PilotingRollData damPRD = new PilotingRollData(entity.getId(), damMod,
entity.damageThisPhase + " damage +" + damMod);
Expand All @@ -16069,13 +16071,24 @@ void checkForPSRFromDamage() {
}
getGame().addControlRoll(damPRD);
} else {
// was the damage threshold exceeded this round?
if (((IAero) entity).wasCritThresh()) {
// was the damage threshold in a single location exceeded this round?
if ((((IAero) entity).wasCritThresh())) {
PilotingRollData damThresh = new PilotingRollData(entity.getId(), 0,
"damage threshold exceeded");
"damage threshold exceeded");
if (entity.hasQuirk(OptionsConstants.QUIRK_POS_EASY_PILOT)
&& (entity.getCrew().getPiloting() > 3)) {
damThresh.addModifier(-1, "easy to pilot");
&& (entity.getCrew().getPiloting() > 3)) {
damThresh.addModifier(-1, "easy to pilot");
}
getGame().addControlRoll(damThresh);
}
if (getGame().getOptions().booleanOption(OptionsConstants.ADVAERORULES_ATMOSPHERIC_CONTROL)
&& entity.damageThisPhase > ((IAero) entity).getHighestThresh()) {
// did the total damage this round exceed the unit's highest threshold?
PilotingRollData damThresh = new PilotingRollData(entity.getId(), 0,
"highest damage threshold exceeded");
if (entity.hasQuirk(OptionsConstants.QUIRK_POS_EASY_PILOT)
&& (entity.getCrew().getPiloting() > 3)) {
damThresh.addModifier(-1, "easy to pilot");
}
getGame().addControlRoll(damThresh);
}
Expand Down Expand Up @@ -16813,18 +16826,22 @@ private Vector<Report> resolveControl(Entity e) {
if (e.isUsingManAce()) {
target.addModifier(-1, "maneuvering ace");
}
for (Enumeration<PilotingRollData> j = game.getControlRolls(); j.hasMoreElements(); ) {
final PilotingRollData modifier = j.nextElement();
if (modifier.getEntityId() != e.getId()) {
continue;
}
// found a roll, add it
rolls.addElement(modifier);
if (reasons.length() > 0) {
reasons.append("; ");
}
reasons.append(modifier.getCumulativePlainDesc());
target.append(modifier);
if (game.getOptions().booleanOption(OptionsConstants.ADVAERORULES_ATMOSPHERIC_CONTROL)) {
addControlWithAdvAtmospheric(e, rolls, reasons);
} else {
for (Enumeration<PilotingRollData> j = game.getControlRolls(); j.hasMoreElements(); ) {
final PilotingRollData modifier = j.nextElement();
if (modifier.getEntityId() != e.getId()) {
continue;
}
// found a roll, add it
rolls.addElement(modifier);
if (!reasons.isEmpty()) {
reasons.append("; ");
}
reasons.append(modifier.getCumulativePlainDesc());
target.append(modifier);
}
}
// any rolls needed?
if (!rolls.isEmpty()) {
Expand Down Expand Up @@ -16990,6 +17007,54 @@ private Vector<Report> resolveControl(Entity e) {
return vReport;
}

/**
* Processes pending control roll events, merging triggers that would be multiple rolls in standard
* rules but are combined into a single roll under the Advanced Atmospheric Control Rolls rule.
*
* @param e The entity whose pending rolls are being processed
* @param rolls List of rolls to be added to
* @param reasons Text representing the reason for the rolls
*/
void addControlWithAdvAtmospheric(Entity e, Vector<PilotingRollData> rolls, StringBuilder reasons) {
// The August 2024 errata changed this rule to only trigger one control roll per
// round regardless of how many crits or thresholds occurred. As a result, the rolls
// need to be combined here at the end phase because not all the information is known
// when the individual rolls are first added throughout the round and previously added
// rolls can't easily be modified.
PilotingRollData atmosphericControlRoll = null;
for (Enumeration<PilotingRollData> j = game.getControlRolls(); j.hasMoreElements(); ) {
final PilotingRollData modifier = j.nextElement();
if (modifier.getEntityId() != e.getId()) {
continue;
}
if (Stream.of("threshold", "avionics hit", "critical hit").anyMatch(s -> modifier.getDesc().contains(s))) {
if (atmosphericControlRoll == null) {
atmosphericControlRoll = modifier;
} else {
// Modify the description of the pending atmospheric control roll instead of adding another one
if (!reasons.isEmpty()) {
reasons.append("; ");
}
reasons.append(modifier.getPlainDesc());
}
} else {
// Not an atmospheric control roll under the new rules, so treat normally
rolls.addElement(modifier);
if (!reasons.isEmpty()) {
reasons.append("; ");
}
reasons.append(modifier.getPlainDesc());
}
}
if (atmosphericControlRoll != null) {
rolls.addElement(atmosphericControlRoll);
if (!reasons.isEmpty()) {
reasons.append("; ");
}
reasons.append(atmosphericControlRoll.getPlainDesc());
}
}

/**
* Check all aircraft that may have used internal bomb bays for incidental explosions
* caused by ground fire.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package megamek.server.totalwarfare;

import megamek.common.*;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.Vector;

import static org.junit.jupiter.api.Assertions.*;

class TWGameManagerTest {
private Player player;
private TWGameManager gameManager;
private Game game;

@BeforeAll
static void before() {
EquipmentType.initializeTypes();
}

@BeforeEach
void setUp() {
player = new Player(0, "Test");
gameManager = new TWGameManager();
game = gameManager.getGame();
game.addPlayer(0, player);
}

@Test
void testAddControlWithAdvAtmosphericMergesIntoOneRollAero() {
AeroSpaceFighter aero = new AeroSpaceFighter();
game.addEntity(aero);
Vector<PilotingRollData> rolls = new Vector<>();
StringBuilder reasons = new StringBuilder();

game.addControlRoll(new PilotingRollData(aero.getId(), 0, "critical hit"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "avionics hit"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "threshold"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "highest damage threshold exceeded"));
gameManager.addControlWithAdvAtmospheric(aero, rolls, reasons);
assertEquals(1, rolls.size());
}

@Test
void testAddControlWithAdvAtmosphericIncludesAllReasonsAero() {
AeroSpaceFighter aero = new AeroSpaceFighter();
game.addEntity(aero);
Vector<PilotingRollData> rolls = new Vector<>();
StringBuilder reasons = new StringBuilder();

game.addControlRoll(new PilotingRollData(aero.getId(), 0, "critical hit"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "avionics hit"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "threshold"));
game.addControlRoll(new PilotingRollData(aero.getId(), 0, "highest damage threshold exceeded"));
gameManager.addControlWithAdvAtmospheric(aero, rolls, reasons);
assertTrue(reasons.toString().contains("critical hit"));
assertTrue(reasons.toString().contains("avionics hit"));
assertTrue(reasons.toString().contains("threshold"));
assertTrue(reasons.toString().contains("highest damage threshold exceeded"));
}

@Test
void testAddControlWithAdvAtmosphericMergesIntoOneRollLAM() {
LandAirMech mech = new LandAirMech(LandAirMech.GYRO_STANDARD, LandAirMech.COCKPIT_STANDARD, LandAirMech.LAM_STANDARD);
game.addEntity(mech);
Vector<PilotingRollData> rolls = new Vector<>();
StringBuilder reasons = new StringBuilder();

game.addControlRoll(new PilotingRollData(mech.getId(), 0, "avionics hit"));
game.addControlRoll(new PilotingRollData(mech.getId(), 0, "threshold"));
game.addControlRoll(new PilotingRollData(mech.getId(), 0, "highest damage threshold exceeded"));
gameManager.addControlWithAdvAtmospheric(mech, rolls, reasons);
assertEquals(1, rolls.size());
}

@Test
void testAddControlWithAdvAtmosphericIncludesAllReasonsLAM() {
LandAirMech mech = new LandAirMech(LandAirMech.GYRO_STANDARD, LandAirMech.COCKPIT_STANDARD, LandAirMech.LAM_STANDARD);
game.addEntity(mech);
Vector<PilotingRollData> rolls = new Vector<>();
StringBuilder reasons = new StringBuilder();

game.addControlRoll(new PilotingRollData(mech.getId(), 0, "avionics hit"));
game.addControlRoll(new PilotingRollData(mech.getId(), 0, "threshold"));
game.addControlRoll(new PilotingRollData(mech.getId(), 0, "highest damage threshold exceeded"));
gameManager.addControlWithAdvAtmospheric(mech, rolls, reasons);
assertTrue(reasons.toString().contains("avionics hit"));
assertTrue(reasons.toString().contains("threshold"));
assertTrue(reasons.toString().contains("highest damage threshold exceeded"));
}
}

0 comments on commit 876cd55

Please sign in to comment.