Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Advanced Atmospheric Control Roll Errata #5867

Merged
merged 18 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to suggestions on the naming/wording for these

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;
Algebro7 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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
16 changes: 15 additions & 1 deletion megamek/src/megamek/common/LandAirMech.java
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public String[] getLocationAbbrs() {
private int straightMoves = 0;
private int altLoss = 0;
private int altLossThisRound = 0;

//Autoejection
private boolean critThresh = false;

Expand Down 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 @@ -19336,9 +19337,10 @@
}
}
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 @@ -19348,13 +19350,24 @@
}
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 @@ -20092,18 +20105,22 @@
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;

Check warning

Code scanning / CodeQL

Dereferenced variable may be null Warning

Variable
nukeStats
may be null at this access as suggested by
this
null guard.
}
// 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 @@ -20269,6 +20286,54 @@
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"));
}
}