diff --git a/megamek/i18n/megamek/common/report-messages.properties b/megamek/i18n/megamek/common/report-messages.properties index f70c32b53d1..48691a57f36 100755 --- a/megamek/i18n/megamek/common/report-messages.properties +++ b/megamek/i18n/megamek/common/report-messages.properties @@ -348,11 +348,11 @@ 3181=Succeeds. 3182=fails, subjecting the unit to particle feedback! 3185= (continuing hit report): -3186=\ - Glancing Blow - +3186=\ - Glancing Blow -\ 9985=\ - Glancing Blow due to Narrow/Low Profile - 3187=does nothing (didn't target a unit). 3188=hits, target tagged. -3189=\ - Direct Blow - +3189=\ - Direct Blow -\ 3190=hits the intended hex . 3191=hits the target in hex with FLAK rounds. 3192=<misses and the FLAK rounds scatter to hex . diff --git a/megamek/src/megamek/client/bot/princess/ArtilleryTargetingControl.java b/megamek/src/megamek/client/bot/princess/ArtilleryTargetingControl.java index 26eb8c76b71..074f5a2e33d 100644 --- a/megamek/src/megamek/client/bot/princess/ArtilleryTargetingControl.java +++ b/megamek/src/megamek/client/bot/princess/ArtilleryTargetingControl.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import megamek.common.*; import megamek.common.actions.ArtilleryAttackAction; @@ -35,6 +36,8 @@ import megamek.common.equipment.WeaponMounted; import megamek.common.options.OptionsConstants; +import static megamek.common.AmmoType.*; + /** * This class handles the creation of firing plans for indirect-fire artillery * and other weapons that get used during the targeting phase. @@ -75,12 +78,12 @@ public class ArtilleryTargetingControl { * shots by accounting for both the original target hex and the computed target * hex in damage calculations. * - * @param damage - * @param hex - * @param shooter - * @param game - * @param owner - * @return + * @param damage Base damage of artillery shot + * @param hex Target {@link Hex} + * @param shooter Attacking {@link Entity} + * @param game {@link Game} instance + * @param owner {@link Princess} instance that owns shooter + * @return Total possible damage this shot can deal based on AE size and base damage */ public double calculateDamageValue(int damage, HexTarget hex, Entity shooter, Game game, Princess owner) { // For leading shots, this will be the computed end point. Might contain @@ -98,12 +101,11 @@ public double calculateDamageValue(int damage, HexTarget hex, Entity shooter, Ga /** * Worker function that calculates the total damage that would be done if a shot * with the given damage value would hit the target coordinates. - * * Caches computation results to avoid repeat * - * @param damage - * @param coords - * @param shooter + * @param damage Base damage of artillery shot + * @param coords {@link Coords} of the target {@link Hex}, used if Hex is off the board + * @param shooter Attacking {@link Entity} * @param game The current {@link Game} * @param owner the {@link Princess} bot to calculate for */ @@ -184,9 +186,9 @@ private double calculateDamageValueForHex(int damage, Coords coords, Entity shoo /** * Cache a calculated damage value for the given damage/coordinates combo * - * @param damage - * @param coords - * @param value + * @param damage Base damage of artillery shot + * @param coords {@link Coords} of the target {@link Hex}, used if Hex is off the board + * @param value Total damage for this attack */ private void cacheDamageValue(int damage, Coords coords, Double value) { if (!damageValues.containsKey(damage)) { @@ -199,9 +201,9 @@ private void cacheDamageValue(int damage, Coords coords, Double value) { /** * Retrieve a calculated damage value for the given damage/coords combo * - * @param damage - * @param coords - * @return + * @param damage Base damage of artillery shot + * @param coords {@link Coords} of the target {@link Hex}, used if Hex is off the board + * @return Calculated total damage */ private Double getDamageValue(int damage, Coords coords) { if (damageValues.containsKey(damage)) { @@ -219,13 +221,13 @@ public void initializeForTargetingPhase() { targetSet = null; } - private boolean getAmmoTypeAvailable(Entity shooter, AmmoType.Munitions munitions) { + private boolean getAmmoTypeAvailable(Entity shooter, Munitions munitions) { boolean available = false; for (WeaponMounted weapon : shooter.getWeaponList()) { if (weapon.getType().hasFlag(WeaponType.F_ARTILLERY)) { for (AmmoMounted ammo : shooter.getAmmo(weapon)) { - if (((AmmoType) ammo.getType()).getMunitionType().contains(munitions) + if (ammo.getType().getMunitionType().contains(munitions) && !weapon.isFired() && ammo.getUsableShotsLeft() > 0) { available = true; break; @@ -245,11 +247,11 @@ private boolean getAmmoTypeAvailable(Entity shooter, AmmoType.Munitions munition * @return true if ADA rounds are available for any weapons, false otherwise */ private boolean getADAAvailable(Entity shooter) { - return getAmmoTypeAvailable(shooter, AmmoType.Munitions.M_ADA); + return getAmmoTypeAvailable(shooter, Munitions.M_ADA); } private boolean getHomingAvailable(Entity shooter) { - return getAmmoTypeAvailable(shooter, AmmoType.Munitions.M_HOMING); + return getAmmoTypeAvailable(shooter, Munitions.M_HOMING); } /** @@ -422,27 +424,34 @@ private FiringPlan calculateIndirectArtilleryPlan(Entity shooter, Game game, Pri for (final AmmoMounted ammo : shooter.getAmmo(currentWeapon)) { // for each enemy unit, evaluate damage value of firing at its hex. // keep track of top target hexes with the same value and fire at them - boolean isADA = ammo.getType().getMunitionType().contains(AmmoType.Munitions.M_ADA); - boolean isSmoke = ammo.getType().getMunitionType().contains(AmmoType.Munitions.M_SMOKE); + boolean isADA = ammo.getType().getMunitionType().contains(Munitions.M_ADA); + boolean isZeroDamageMunition = ( + Stream.of(SMOKE_MUNITIONS, FLARE_MUNITIONS, MINE_MUNITIONS).anyMatch( + munitions -> munitions.containsAll(ammo.getType().getMunitionType()) + ) + ); for (Targetable target : targetSet) { - WeaponFireInfo wfi; - double damageValue = 0.0; - if (target.getTargetType() == Targetable.TYPE_ENTITY) { - damageValue = damage; + double damageValue; + if (isZeroDamageMunition) { + // Skip zero-damage utility munitions for now. + // XXX: update when utility munition handling goes in + damageValue = 0.0; } else { - if (!(isADA || isSmoke)) { - damageValue = calculateDamageValue(damage, (HexTarget) target, shooter, game, owner); - } else if (isSmoke) { - damageValue = 0; + if (target.getTargetType() == Targetable.TYPE_ENTITY) { + damageValue = damage; } else { - // No ADA attacks except at Entities. - continue; + if (!isADA) { + damageValue = calculateDamageValue(damage, (HexTarget) target, shooter, game, owner); + } else { + // No ADA attacks except at Entities. + continue; + } } } // ADA attacks should be handled as Direct Fire but we'll calc hits here for // comparison. - wfi = new WeaponFireInfo(shooter, target, currentWeapon, ammo, game, false, owner); + WeaponFireInfo wfi = new WeaponFireInfo(shooter, target, currentWeapon, ammo, game, false, owner); // factor the chance to hit when picking a target - if we've got a spotted hex // or an auto-hit hex @@ -452,8 +461,8 @@ private FiringPlan calculateIndirectArtilleryPlan(Entity shooter, Game game, Pri damageValue *= wfi.getProbabilityToHit(); if (damageValue > maxDamage) { - if (((AmmoType) (wfi.getAmmo().getType())).getMunitionType() - .contains(AmmoType.Munitions.M_HOMING)) { + if ((wfi.getAmmo().getType()).getMunitionType() + .contains(Munitions.M_HOMING)) { wfi.getAmmo().setSwitchedReason(1505); } else { wfi.getAmmo().setSwitchedReason(1503); @@ -468,8 +477,8 @@ private FiringPlan calculateIndirectArtilleryPlan(Entity shooter, Game game, Pri topValuedFireInfos.add(wfi); } } else if ((damageValue == maxDamage) && (damageValue > 0)) { - if (((AmmoType) wfi.getAmmo().getType()).getMunitionType() - .contains(AmmoType.Munitions.M_ADA)) { + if (wfi.getAmmo().getType().getMunitionType() + .contains(Munitions.M_ADA)) { topValuedADAInfos.add(wfi); } else { topValuedFireInfos.add(wfi); @@ -489,7 +498,7 @@ private FiringPlan calculateIndirectArtilleryPlan(Entity shooter, Game game, Pri actualFireInfo = topValuedFireInfos.get(0); } else { actualFireInfo = topValuedFireInfos.get(Compute.randomInt(topValuedFireInfos.size())); - if (actualFireInfo.getAmmo() != actualFireInfo.getWeapon().getLinked()) { + if (!actualFireInfo.getAmmo().equals(actualFireInfo.getWeapon().getLinked())) { // Announce why we switched actualFireInfo.getAmmo().setSwitchedReason(1507); } @@ -545,10 +554,10 @@ private FiringPlan calculateIndirectArtilleryPlan(Entity shooter, Game game, Pri * Worker function that calculates the shooter's "best" actions that result in a * TAG being fired. * - * @param shooter - * @param game The current {@link Game} - * @param owner - * @return + * @param shooter Attacking {@link Entity} + * @param game The current {@link Game} + * @param owner {@link Princess} instance that owns shooter + * @return Highest hit-chance TAG attack's {@link WeaponFireInfo} */ private WeaponFireInfo getTAGInfo(WeaponMounted weapon, Entity shooter, Game game, Princess owner) { WeaponFireInfo returnValue = null; @@ -569,9 +578,9 @@ private WeaponFireInfo getTAGInfo(WeaponMounted weapon, Entity shooter, Game gam private static class HelperAmmo { public int equipmentNum; - public EnumSet munitionType = EnumSet.noneOf(AmmoType.Munitions.class); + public EnumSet munitionType; - public HelperAmmo(int equipmentNum, EnumSet munitionType) { + public HelperAmmo(int equipmentNum, EnumSet munitionType) { this.equipmentNum = equipmentNum; this.munitionType = munitionType; } @@ -581,23 +590,23 @@ public HelperAmmo(int equipmentNum, EnumSet munitionType) { * Worker function that selects the appropriate ammo for the given entity and * weapon. * - * @param shooter - * @param currentWeapon - * @return + * @param shooter Attacking {@link Entity} + * @param currentWeapon {@link Mounted} instance being used for this attack + * @return {@link AmmoMounted} to be used to attack with this weapon */ private HelperAmmo findAmmo(Entity shooter, Mounted currentWeapon, Mounted preferredAmmo) { int ammoEquipmentNum = NO_AMMO; - EnumSet ammoMunitionType = EnumSet.noneOf(AmmoType.Munitions.class); + EnumSet ammoMunitionType = EnumSet.noneOf(Munitions.class); if (preferredAmmo != null && preferredAmmo.isAmmoUsable() && - AmmoType.isAmmoValid(preferredAmmo, (WeaponType) currentWeapon.getType())) { + isAmmoValid(preferredAmmo, (WeaponType) currentWeapon.getType())) { // Use the ammo we used for calculations. ammoEquipmentNum = shooter.getEquipmentNum(preferredAmmo); ammoMunitionType = ((AmmoType) preferredAmmo.getType()).getMunitionType(); } else { // simply grab the first valid ammo and let 'er rip. for (Mounted ammo : shooter.getAmmo()) { - if (!ammo.isAmmoUsable() || !AmmoType.isAmmoValid(ammo, (WeaponType) currentWeapon.getType())) { + if (!ammo.isAmmoUsable() || !isAmmoValid(ammo, (WeaponType) currentWeapon.getType())) { continue; } @@ -607,7 +616,7 @@ private HelperAmmo findAmmo(Entity shooter, Mounted currentWeapon, Mounted // TODO: Attempt to select homing ammo if the target is tagged. // To do so, check - // ammoType.getMunitionType().contains(AmmoType.Munitions.M_HOMING) + // ammoType.getMunitionType().contains(Munitions.M_HOMING) } } @@ -618,9 +627,9 @@ private HelperAmmo findAmmo(Entity shooter, Mounted currentWeapon, Mounted * Function that calculates the potential damage if an artillery attack * were to land on target. * - * @param coords - * @param operator - * @return + * @param coords {@link Coords} of the target {@link Hex}, used if Hex is off the board + * @param operator {@link Princess} instance who is checking for incoming artillery damage + * @return Damage value calculated from incoming shots that may hit these coordinates */ public static double evaluateIncomingArtilleryDamage(Coords coords, Princess operator) { double sum = 0; @@ -637,13 +646,13 @@ public static double evaluateIncomingArtilleryDamage(Coords coords, Princess ope if ((aaa.getTurnsTilHit() == 0) && (aaa.getTarget(operator.getGame()) != null)) { // damage for artillery weapons is, for some reason, derived from the weapon // type's rack size - int damage = 0; + int damage; Mounted weapon = aaa.getEntity(operator.getGame()).getEquipment(aaa.getWeaponId()); if (null == weapon) { // The weaponId couldn't get us a weapon; probably a bomb Arrow IV dropped on a // prior turn. BombType bombType = BombType.createBombByType(BombType.getBombTypeFromName("Arrow IV Missile")); - damage = bombType.getRackSize(); + damage = (bombType != null) ? bombType.getRackSize() : 0; } else { if (weapon.getType() instanceof BombType) { damage = (weapon.getExplosionDamage()); @@ -663,7 +672,7 @@ public static double evaluateIncomingArtilleryDamage(Coords coords, Princess ope artySkill = aaa.getEntity(operator.getGame()).getCrew().getArtillery(); } - double hitOdds = 0.0; + double hitOdds; if (operator.getArtilleryAutoHit() != null && operator.getArtilleryAutoHit().contains(coords)) { hitOdds = 1.0; diff --git a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java index 6cc6d0ec8c1..04e5d9c3dd6 100644 --- a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java +++ b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java @@ -21,14 +21,12 @@ import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; +import java.util.*; import megamek.common.*; import megamek.common.actions.ArtilleryAttackAction; import megamek.common.actions.WeaponAttackAction; +import megamek.common.AmmoType.*; import megamek.common.annotations.Nullable; import megamek.common.equipment.AmmoMounted; import megamek.common.equipment.BombMounted; @@ -39,6 +37,8 @@ import megamek.common.weapons.infantry.InfantryWeaponHandler; import megamek.logging.MMLogger; +import static megamek.common.AmmoType.*; + /** * WeaponFireInfo is a wrapper around a WeaponAttackAction that includes * probability to hit and expected damage @@ -418,6 +418,19 @@ private WeaponAttackAction buildBombAttackAction(final HashMap bo return computeExpectedBombDamage(getShooter(), weapon, getTarget().getPosition()); } + // XXX: update this and other utility munition handling with smarter deployment, a la TAG above + if (preferredAmmo != null) { + var munitionType = preferredAmmo.getType().getMunitionType(); + // Handle all 0-damage munitions here + if ( + SMOKE_MUNITIONS.containsAll(munitionType) || + FLARE_MUNITIONS.containsAll(munitionType) || + MINE_MUNITIONS.containsAll(munitionType) + ) { + return 0D; + } + } + // bay weapons require special consideration, by looping through all weapons and // adding up the damage // A bay's weapons may have different ranges, most noticeable in laser bays, diff --git a/megamek/src/megamek/common/AmmoType.java b/megamek/src/megamek/common/AmmoType.java index 3ba85d60c2c..c302571e39e 100644 --- a/megamek/src/megamek/common/AmmoType.java +++ b/megamek/src/megamek/common/AmmoType.java @@ -339,6 +339,17 @@ public enum Munitions { M_FAE } + public static final EnumSet SMOKE_MUNITIONS = EnumSet.of(AmmoType.Munitions.M_SMOKE, AmmoType.Munitions.M_SMOKE_WARHEAD); + public static final EnumSet FLARE_MUNITIONS = EnumSet.of(AmmoType.Munitions.M_FLARE); + public static final EnumSet MINE_MUNITIONS = EnumSet.of( + AmmoType.Munitions.M_THUNDER, + AmmoType.Munitions.M_THUNDER_ACTIVE, + AmmoType.Munitions.M_THUNDER_AUGMENTED, + AmmoType.Munitions.M_THUNDER_INFERNO, + AmmoType.Munitions.M_THUNDER_VIBRABOMB, + AmmoType.Munitions.M_FASCAM + ); + private static Vector> m_vaMunitions = new Vector<>(NUM_TYPES); public static Vector getMunitionsFor(int nAmmoType) { diff --git a/megamek/src/megamek/common/Minefield.java b/megamek/src/megamek/common/Minefield.java index afb1b171be7..d181e0a3389 100644 --- a/megamek/src/megamek/common/Minefield.java +++ b/megamek/src/megamek/common/Minefield.java @@ -36,13 +36,13 @@ public class Minefield implements Serializable, Cloneable { public static final int CLEAR_NUMBER_INF_ENG_ACCIDENT = 3; public static final int CLEAR_NUMBER_BA_SWEEPER = 6; public static final int CLEAR_NUMBER_BA_SWEEPER_ACCIDENT = 2; - + public static final int BAP_DETECT_TARGET_PREPLACED = 10; public static final int BAP_DETECT_TARGET_WEAPON_DELIVERED = 7; public static final int TO_HIT_SIDE = ToHitData.SIDE_FRONT; public static final int TO_HIT_TABLE = ToHitData.HIT_KICK; - + public static final int HOVER_WIGE_DETONATION_TARGET = 12; public static final int MAX_DAMAGE = 30; @@ -53,7 +53,7 @@ public class Minefield implements Serializable, Cloneable { "Vibrabomb", "Active", "EMP", "Inferno"}; //"Thunder", "Thunder-Inferno", "Thunder-Active", //"Thunder-Vibrabomb" }; - + public static int TYPE_SIZE = names.length; private Coords coords = null; @@ -72,22 +72,22 @@ public class Minefield implements Serializable, Cloneable { private Minefield() { //Creates a minefield } - + public static Minefield createMinefield(Coords coords, int playerId, int type, int density) { return createMinefield(coords, playerId, type, density, 0); } - + public static Minefield createMinefield(Coords coords, int playerId, int type, int density, boolean sea, int depth) { return createMinefield(coords, playerId, type, density, 0, sea, depth); } - + public static Minefield createMinefield(Coords coords, int playerId, int type, int density, int setting) { return createMinefield(coords, playerId, type, density, setting, false, 0); } public static Minefield createMinefield(Coords coords, int playerId, int type, int density, int setting, boolean sea, int depth) { Minefield mf = new Minefield(); - + mf.type = type; mf.density = density; mf.coords = coords; @@ -97,8 +97,24 @@ public static Minefield createMinefield(Coords coords, int playerId, int type, i mf.depth = depth; return mf; } - - + + @Override + public String toString() { + return "Minefield{'" + + getDisplayableName(type) + + "', coords=" + coords + + ", playerId=" + playerId + + ", density=" + density + + ", type=" + type + + ", setting=" + setting + + ", oneUse=" + oneUse + + ", sea=" + sea + + ", depth=" + depth + + ", detonated=" + detonated + + ", weaponDelivered=" + weaponDelivered + + '}'; + } + public static String getDisplayableName(int type) { if (type >= 0 && type < TYPE_SIZE) { return names[type]; @@ -131,10 +147,10 @@ public boolean equals(Object obj) { return false; } final Minefield other = (Minefield) obj; - return (playerId == other.playerId) && Objects.equals(coords, other.coords) && + return (playerId == other.playerId) && Objects.equals(coords, other.coords) && (type == other.type); } - + @Override public int hashCode() { return Objects.hash(playerId, coords, type); @@ -156,7 +172,7 @@ public int getDensity() { * what do we need to roll to trigger this mine * @return */ - public int getTrigger() { + public int getTrigger() { if (density < 15) { return 9; } else if (density < 25) { @@ -181,7 +197,7 @@ public int getSetting() { public int getType() { return type; } - + public int getDepth() { return depth; } @@ -193,23 +209,23 @@ public String getName() { public int getPlayerId() { return playerId; } - + public void setDetonated(boolean b) { this.detonated = b; } - + public boolean hasDetonated() { return detonated; } - + public void setWeaponDelivered(boolean b) { this.weaponDelivered = b; } - + public boolean isWeaponDelivered() { return weaponDelivered; } - + /** * check for a reduction in density * @param bonus - an int indicating the modifier to the target roll for reduction @@ -223,16 +239,16 @@ public void checkReduction(int bonus, boolean direct) { setDensity(getDensity() - 5); return; } - + boolean isReduced = ((Compute.d6(2) + bonus) >= getTrigger()) || (direct && getType() != Minefield.TYPE_CONVENTIONAL && getType() != Minefield.TYPE_INFERNO); if (getType() == Minefield.TYPE_CONVENTIONAL && getDensity() < 10) { isReduced = false; } if (isReduced) { setDensity(getDensity() - 5); - } + } } - + /** * Gets the BAP detection target # */ diff --git a/megamek/src/megamek/server/totalwarfare/TWGameManager.java b/megamek/src/megamek/server/totalwarfare/TWGameManager.java index 78bd3351a6f..c8213e82ba1 100644 --- a/megamek/src/megamek/server/totalwarfare/TWGameManager.java +++ b/megamek/src/megamek/server/totalwarfare/TWGameManager.java @@ -7122,13 +7122,20 @@ private boolean enterMinefield(Entity entity, Coords c, int curElev, boolean isO continue; } - // if we are in the water, then the sea mine will only blow up if at - // the right depth - if (game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.WATER)) { - if ((Math.abs(curElev) != mf.getDepth()) + try { + // if we are in the water, then the sea mine will only blow up if at + // the right depth + if (game.getBoard().getHex(mf.getCoords()) != null && + game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.WATER) + ) { + if ((Math.abs(curElev) != mf.getDepth()) && (Math.abs(curElev + entity.getHeight()) != mf.getDepth())) { - continue; + continue; + } } + } catch (NullPointerException _ignored) { + logger.warn("Minefield not found on board: " + mf.toString()); + continue; } // Check for mine-sweeping. Vibramines handled elsewhere @@ -7596,16 +7603,23 @@ boolean checkVibrabombs(Entity entity, Coords coords, boolean displaced, Coords while (e.hasMoreElements()) { Minefield mf = e.nextElement(); - // Bug 954272: Mines shouldn't work underwater, and BMRr says - // Vibrabombs are mines - if (game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.WATER) + try { + // Bug 954272: Mines shouldn't work underwater, and BMRr says + // Vibrabombs are mines + if (game.getBoard().getHex(mf.getCoords()) != null && + game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.WATER) && !game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.PAVEMENT) && !game.getBoard().getHex(mf.getCoords()).containsTerrain(Terrains.ICE)) { - continue; - } + continue; + } + + // Mek weighing 10 tons or less can't set off the bomb + if (mass <= (mf.getSetting() - 10)) { + continue; + } - // Mek weighing 10 tons or less can't set off the bomb - if (mass <= (mf.getSetting() - 10)) { + } catch (NullPointerException _ignored) { + logger.warn("Minefield not found on board: " + mf.toString()); continue; }