diff --git a/megamek/src/megamek/client/ui/swing/CustomMechDialog.java b/megamek/src/megamek/client/ui/swing/CustomMechDialog.java index d8d7bd99980..a2c196429e2 100644 --- a/megamek/src/megamek/client/ui/swing/CustomMechDialog.java +++ b/megamek/src/megamek/client/ui/swing/CustomMechDialog.java @@ -24,6 +24,7 @@ import megamek.common.util.fileUtils.MegaMekFile; import megamek.common.verifier.*; import megamek.common.weapons.bayweapons.ArtilleryBayWeapon; +import megamek.common.weapons.bayweapons.BayWeapon; import megamek.common.weapons.bayweapons.CapitalMissileBayWeapon; import javax.swing.*; @@ -34,6 +35,7 @@ import java.awt.event.*; import java.util.List; import java.util.*; +import java.util.stream.Collectors; /** * A dialog that a player can use to customize his mech before battle. @@ -326,27 +328,13 @@ private void addOption(IOption option, GridBagLayout gridbag, GridBagConstraints if ((OptionsConstants.GUNNERY_WEAPON_SPECIALIST).equals(option.getName())) { optionComp.addValue(Messages.getString("CustomMechDialog.None")); - TreeSet uniqueWeapons = new TreeSet<>(); - for (int i = 0; i < entity.getWeaponList().size(); i++) { - Mounted m = entity.getWeaponList().get(i); - uniqueWeapons.add(m.getName()); - } - for (String name : uniqueWeapons) { - optionComp.addValue(name); - } + PilotSPAHelper.weaponSpecialistValidWeaponNames(entity, gameOptions()).forEach(optionComp::addValue); optionComp.setSelected(option.stringValue()); } if ((OptionsConstants.GUNNERY_SANDBLASTER).equals(option.getName())) { optionComp.addValue(Messages.getString("CustomMechDialog.None")); - TreeSet uniqueWeapons = new TreeSet<>(); - for (int i = 0; i < entity.getWeaponList().size(); i++) { - Mounted m = entity.getWeaponList().get(i); - uniqueWeapons.add(m.getName()); - } - for (String name : uniqueWeapons) { - optionComp.addValue(name); - } + PilotSPAHelper.sandblasterValidWeaponNames(entity, gameOptions()).forEach(optionComp::addValue); optionComp.setSelected(option.stringValue()); } diff --git a/megamek/src/megamek/common/PilotSPAHelper.java b/megamek/src/megamek/common/PilotSPAHelper.java new file mode 100644 index 00000000000..b6bffdef6af --- /dev/null +++ b/megamek/src/megamek/common/PilotSPAHelper.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2023 - The MegaMek Team. All Rights Reserved. + * + * This file is part of MegaMek. + * + * MegaMek is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * MegaMek is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with MegaMek. If not, see . + */ +package megamek.common; + +import megamek.common.annotations.Nullable; +import megamek.common.options.GameOptions; +import megamek.common.options.OptionsConstants; +import megamek.common.weapons.autocannons.ACWeapon; +import megamek.common.weapons.autocannons.LBXACWeapon; +import megamek.common.weapons.autocannons.UACWeapon; +import megamek.common.weapons.bayweapons.BayWeapon; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * This class contains helper methods for Special Pilot Abilities. + */ +public final class PilotSPAHelper { + + /** @return True when the given Mounted equipment is a valid choice for the Weapons Specialist SPA. */ + public static boolean isWeaponSpecialistValid(Mounted mounted, @Nullable GameOptions options) { + return isWeaponSpecialistValid(mounted.getType(), options); + } + + /** @return True when the given EquipmentType is a valid choice for the Weapons Specialist SPA. */ + public static boolean isWeaponSpecialistValid(EquipmentType equipmentType, @Nullable GameOptions options) { + boolean amsAsWeapon = (options != null) && options.booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_MANUAL_AMS) + && (equipmentType.hasFlag(WeaponType.F_AMS)); + + return (equipmentType instanceof WeaponType) && !(equipmentType instanceof BayWeapon) + && (!equipmentType.hasFlag(WeaponType.F_AMS) || amsAsWeapon) + && !equipmentType.is("Screen Launcher") + && !equipmentType.hasFlag(WeaponType.F_C3M) && !equipmentType.hasFlag(WeaponType.F_C3MBS) + && !equipmentType.hasFlag(WeaponType.F_INFANTRY_ATTACK); + } + + /** + * Returns a List of distinct (each occuring only once) weapon names of weapons present on the given + * Entity that are valid choices for the Weapon Specialist SPA. + * + * @return A list of weapon names from the given Entity that are valid choices for the Weapon Specialist SPA + */ + public static List weaponSpecialistValidWeaponNames(Entity entity, @Nullable GameOptions options) { + return entity.getTotalWeaponList().stream() + .map(Mounted::getType) + .filter(mounted -> isWeaponSpecialistValid(mounted, options)) + .map(EquipmentType::getName) + .distinct() + .collect(Collectors.toList()); + } + + /** + * Returns a List of weapons from those present on the given Entity that are valid choices for the + * Weapon Specialist SPA. Unlike {@link #weaponSpecialistValidWeaponNames(Entity, GameOptions)}, weapons + * appear in this list as often as they are present on the given Entity. + * + * @return A list of weapons from the given Entity that are valid choices for the Weapon Specialist SPA + */ + public static List weaponSpecialistValidWeapons(Entity entity, @Nullable GameOptions options) { + return entity.getTotalWeaponList().stream() + .filter(mounted -> isWeaponSpecialistValid(mounted, options)) + .collect(Collectors.toList()); + } + + /** + * Returns true when the given Mounted equipment is a valid choice for the Sandblaster SPA, taking into account + * the given GameOptions, particularly, if TacOps RapidFire Autocannons is in use. When the given GameOptions + * is null, TacOps RapidFire Autocannons is assumed off. When TacOps RapidFire Autocannons is off, + * standard ACs are considered invalid. + * + * @return True when the given EquipmentType is a valid choice for the Sandblaster SPA. + */ + public static boolean isSandblasterValid(Mounted mounted, @Nullable GameOptions options) { + return isSandblasterValid(mounted.getType(), options); + } + + /** + * Returns true when the given EquipmentType is a valid choice for the Sandblaster SPA, taking into account + * the given GameOptions, particularly, if TacOps RapidFire Autocannons is in use. When the given GameOptions + * is null, TacOps RapidFire Autocannons is assumed off. When TacOps RapidFire Autocannons is off, + * standard ACs are considered invalid. + * + * @return True when the given EquipmentType is a valid choice for the Sandblaster SPA. + */ + public static boolean isSandblasterValid(EquipmentType equipmentType, @Nullable GameOptions options) { + boolean rapidFireAC = (options != null) && options.booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RAPID_AC) + && (equipmentType instanceof ACWeapon); + + return (equipmentType instanceof WeaponType) + && ((equipmentType instanceof UACWeapon) || (equipmentType instanceof LBXACWeapon) || rapidFireAC + || ((WeaponType) equipmentType).damage == WeaponType.DAMAGE_BY_CLUSTERTABLE); + } + + /** + * Returns a List of distinct (each occuring only once) weapon names of weapons present on the given + * Entity that are valid choices for the Sandblaster SPA. + * + * @return A list of weapon names from the given Entity that are valid choices for the Sandblaster SPA + */ + public static List sandblasterValidWeaponNames(Entity entity, @Nullable GameOptions options) { + return entity.getTotalWeaponList().stream() + .filter(mounted -> isSandblasterValid(mounted, options)) + .map(Mounted::getName) + .distinct() + .collect(Collectors.toList()); + } + + /** + * Returns a List of weapons from those present on the given Entity that are valid choices for the + * Sandblaster SPA. Unlike {@link #sandblasterValidWeaponNames(Entity, GameOptions)}, weapons + * appear in this list as often as they are present on the given Entity. + * + * @return A list of weapons from the given Entity that are valid choices for the Sandblaster SPA + */ + public static List sandblasterValidWeapons(Entity entity, @Nullable GameOptions options) { + return entity.getTotalWeaponList().stream() + .filter(mounted -> isSandblasterValid(mounted, options)) + .collect(Collectors.toList()); + } + + private PilotSPAHelper() { } +} \ No newline at end of file diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 505f0259ad8..255b72c99fb 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -23,10 +23,7 @@ import megamek.common.weapons.Weapon; import megamek.common.weapons.artillery.ArtilleryCannonWeapon; import megamek.common.weapons.artillery.ArtilleryWeapon; -import megamek.common.weapons.bayweapons.LaserBayWeapon; -import megamek.common.weapons.bayweapons.PPCBayWeapon; -import megamek.common.weapons.bayweapons.PulseLaserBayWeapon; -import megamek.common.weapons.bayweapons.ScreenLauncherBayWeapon; +import megamek.common.weapons.bayweapons.*; import megamek.common.weapons.capitalweapons.CapitalMissileWeapon; import megamek.common.weapons.gaussrifles.GaussWeapon; import megamek.common.weapons.gaussrifles.ISHGaussRifle; @@ -717,8 +714,8 @@ private static ToHitData toHitCalc(Game game, int attackerId, Targetable target, toHit = compileEnvironmentalToHitMods(game, ae, target, wtype, atype, toHit, isArtilleryIndirect); // Collect the modifiers for the crew/pilot - toHit = compileCrewToHitMods(game, ae, te, toHit, wtype); - + toHit = compileCrewToHitMods(game, ae, te, toHit, weapon); + // Collect the modifiers for the attacker's condition/actions if (ae != null) { //Conventional fighter, Aerospace and fighter LAM attackers @@ -3945,12 +3942,11 @@ else if (wtype.getAtClass() == WeaponType.CLASS_LBX_AC) { * @param ae The Entity making this attack * @param te The target Entity * @param toHit The running total ToHitData for this WeaponAttackAction - * - * @param wtype The WeaponType of the weapon being used - * + * @param weapon The weapon being used (it's type should be WeaponType!) + * */ - private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, ToHitData toHit, WeaponType wtype) { - + private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, ToHitData toHit, Mounted weapon) { + if (ae == null) { // These checks won't work without a valid attacker return toHit; @@ -4014,6 +4010,8 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T toHit.addModifier(-1, Messages.getString("WeaponAttackAction.Vdni")); } + WeaponType wtype = ((weapon != null) && (weapon.getType() instanceof WeaponType)) ? (WeaponType) weapon.getType() : null; + if (ae.isConventionalInfantry()) { // check for cyber eye laser sighting on ranged attacks if (ae.hasAbility(OptionsConstants.MD_CYBER_IMP_LASER) @@ -4041,15 +4039,17 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T } // Is the pilot a weapon specialist? - if (wtype != null && ae.hasAbility(OptionsConstants.GUNNERY_WEAPON_SPECIALIST, wtype.getName())) { + if (wtype instanceof BayWeapon + && weapon.getBayWeapons().stream().map(ae::getEquipment) + .allMatch(w -> ae.hasAbility(OptionsConstants.GUNNERY_WEAPON_SPECIALIST, w.getName()))) { + // All weapons in a bay must match the specialization + toHit.addModifier(-2, Messages.getString("WeaponAttackAction.WeaponSpec")); + } else if (wtype != null && ae.hasAbility(OptionsConstants.GUNNERY_WEAPON_SPECIALIST, wtype.getName())) { toHit.addModifier(-2, Messages.getString("WeaponAttackAction.WeaponSpec")); } else if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST)) { - // aToW style gunnery specialist: -1 to specialized weapon and +1 to - // all other weapons - // Note that weapon specialist supersedes gunnery specialization, so - // if you have - // a specialization in Medium Lasers and a Laser specialization, you - // only get the -2 specialization mod + // aToW style gunnery specialist: -1 to specialized weapon and +1 to all other weapons + // Note that weapon specialist supersedes gunnery specialization, so if you have + // a specialization in Medium Lasers and a Laser specialization, you only get the -2 specialization mod if (wtype != null && wtype.hasFlag(WeaponType.F_ENERGY)) { if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_ENERGY)) { toHit.addModifier(-1, Messages.getString("WeaponAttackAction.EnergySpec")); diff --git a/megamek/src/megamek/common/options/Option.java b/megamek/src/megamek/common/options/Option.java index 2f0f9a9c468..de99ea44594 100755 --- a/megamek/src/megamek/common/options/Option.java +++ b/megamek/src/megamek/common/options/Option.java @@ -87,7 +87,7 @@ public String getName() { public String getDisplayableNameWithValue() { updateInfo(); return info.getDisplayableName() - + (type == IOption.INTEGER ? " " + value : ""); + + ((type == IOption.INTEGER) || (type == IOption.CHOICE) ? " [" + value + "]" : ""); } @Override