diff --git a/megamek/src/megamek/client/ui/swing/QuirksPanel.java b/megamek/src/megamek/client/ui/swing/QuirksPanel.java index f042696d363..d18f2f23804 100644 --- a/megamek/src/megamek/client/ui/swing/QuirksPanel.java +++ b/megamek/src/megamek/client/ui/swing/QuirksPanel.java @@ -74,7 +74,7 @@ public void refreshQuirks() { for (Enumeration j = group.getSortedOptions(); j.hasMoreElements(); ) { IOption option = j.nextElement(); - if (!Quirks.isQuirkLegalFor(option, entity)) { + if (null == option || !Quirks.isQuirkLegalFor(option, entity)) { continue; } diff --git a/megamek/src/megamek/client/ui/swing/tooltip/TipUtil.java b/megamek/src/megamek/client/ui/swing/tooltip/TipUtil.java index 1ea22934714..658aa67e35f 100644 --- a/megamek/src/megamek/client/ui/swing/tooltip/TipUtil.java +++ b/megamek/src/megamek/client/ui/swing/tooltip/TipUtil.java @@ -124,7 +124,7 @@ private static String optionListFull(Enumeration advGroups, List origList = new ArrayList<>(); for (Enumeration advs = advGroup.getOptions(); advs.hasMoreElements();) { IOption adv = advs.nextElement(); - if (adv.booleanValue()) { + if (adv != null && adv.booleanValue()) { origList.add(adv.getDisplayableNameWithValue()); } } diff --git a/megamek/src/megamek/common/QuirksHandler.java b/megamek/src/megamek/common/QuirksHandler.java index 40c93088282..6f627cc6e59 100644 --- a/megamek/src/megamek/common/QuirksHandler.java +++ b/megamek/src/megamek/common/QuirksHandler.java @@ -542,7 +542,7 @@ public static void addCustomQuirk(Entity entity, boolean useModel) { while (quirkOptions.hasMoreElements()) { IOption option = quirkOptions.nextElement(); // Ignore illegal quirks, and ones that aren't set - if (!Quirks.isQuirkLegalFor(option, entity) || !option.booleanValue()) { + if (option == null || !(Quirks.isQuirkLegalFor(option, entity) && option.booleanValue())) { continue; } // Add new QuirkEntry diff --git a/megamek/src/megamek/common/actions/WeaponAttackAction.java b/megamek/src/megamek/common/actions/WeaponAttackAction.java index 18fbe60125c..992ef1da391 100644 --- a/megamek/src/megamek/common/actions/WeaponAttackAction.java +++ b/megamek/src/megamek/common/actions/WeaponAttackAction.java @@ -30,9 +30,7 @@ import megamek.common.weapons.lasers.ISBombastLaser; import megamek.common.weapons.lasers.VariableSpeedPulseLaserWeapon; import megamek.common.weapons.lrms.LRTWeapon; -import megamek.common.weapons.mortars.MekMortarWeapon; import megamek.common.weapons.srms.SRTWeapon; -import org.apache.commons.lang.ArrayUtils; import org.apache.logging.log4j.LogManager; import java.io.Serializable; @@ -3077,106 +3075,8 @@ private static ToHitData compileWeaponToHitMods(Game game, Entity ae, Entity spo } // VSP Lasers + // Quirks and SPAs now handled in toHit - // SPA Environmental Specialist - // Fog Specialist - if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_FOG) - && wtype.hasFlag(WeaponType.F_ENERGY) && !game.getBoard().inSpace() - && (game.getPlanetaryConditions().getFog() == PlanetaryConditions.FOG_HEAVY)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.FogSpec")); - } - - // Light Specialist - if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_LIGHT)) { - if ((te != null) && !te.isIlluminated() - && ((game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_DUSK) - || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_FULL_MOON) - || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_MOONLESS) - || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_PITCH_BLACK))) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.LightSpec")); - } else if ((te != null) && te.isIlluminated() - && (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_PITCH_BLACK)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.LightSpec")); - } - } - - // Rain Specialist - if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_RAIN)) { - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_LIGHT_RAIN) - && ae.isConventionalInfantry()) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.RainSpec")); - } - - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_MOD_RAIN) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_HEAVY_RAIN) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_GUSTING_RAIN) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_DOWNPOUR)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.RainSpec")); - } - } - - // Snow Specialist - if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_SNOW)) { - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_LIGHT_SNOW) - && ae.isConventionalInfantry()) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); - } - - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_ICE_STORM) - && wtype.hasFlag(WeaponType.F_MISSILE)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); - } - - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_SLEET) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_SNOW_FLURRIES) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_MOD_SNOW) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_HEAVY_SNOW)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); - } - } - - // Wind Specialist - if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_WIND)) { - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_MOD_GALE) - && wtype.hasFlag(WeaponType.F_MISSILE)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); - } - - if (wtype.hasFlag(WeaponType.F_MISSILE) && wtype.hasFlag(WeaponType.F_BALLISTIC) - && ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_STRONG_GALE) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_STORM))) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.WindSpec")); - } - - if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_TORNADO_F13) - || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_TORNADO_F4)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.WindSpec")); - } - } - - - // quirks - - // Flat -1 for Accurate Weapon - if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_ACCURATE)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.AccWeapon")); - } - // Flat +1 for Inaccurate Weapon - if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_INACCURATE)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.InAccWeapon")); - } - // Stable Weapon - Reduces running/flanking penalty by 1 - if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_STABLE_WEAPON) && (ae.moved == EntityMovementType.MOVE_RUN)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.StableWeapon")); - } - // +1 for a Misrepaired Weapon - See StratOps Partial Repairs - if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_MISREPAIRED)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.MisrepairedWeapon")); - } - // +1 for a Misreplaced Weapon - See StratOps Partial Repairs - if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_MISREPLACED)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.MisreplacedWeapon")); - } return toHit; } @@ -3590,20 +3490,6 @@ private static ToHitData compileAttackerToHitMods(Game game, Entity ae, Targetab } } - // Quirks - - // Anti-air targeting quirk vs airborne unit - if (ae.hasQuirk(OptionsConstants.QUIRK_POS_ANTI_AIR) && (target instanceof Entity)) { - if (target.isAirborneVTOLorWIGE() || target.isAirborne()) { - toHit.addModifier(-2, Messages.getString("WeaponAttackAction.AaVsAir")); - } - } - - // Sensor ghosts quirk - if (ae.hasQuirk(OptionsConstants.QUIRK_NEG_SENSOR_GHOSTS)) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorGhosts")); - } - return toHit; } @@ -4044,90 +3930,6 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T } } - // SPAs - - // Unofficial weapon class specialist - Does not have an unspecialized penalty - if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_LASER) - && wtype != null && wtype.hasFlag(WeaponType.F_ENERGY)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunLSkill")); - } - - if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_BALLISTIC) - && wtype != null && wtype.hasFlag(WeaponType.F_BALLISTIC)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunBSkill")); - } - - if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_MISSILE) - && wtype != null && wtype.hasFlag(WeaponType.F_MISSILE)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunMSkill")); - } - - // Is the pilot a weapon specialist? - 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 - if (wtype != null && wtype.hasFlag(WeaponType.F_ENERGY)) { - if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_ENERGY)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.EnergySpec")); - } else { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); - } - } else if (wtype != null && wtype.hasFlag(WeaponType.F_BALLISTIC)) { - if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_BALLISTIC)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BallisticSpec")); - } else { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); - } - } else if (wtype != null && wtype.hasFlag(WeaponType.F_MISSILE)) { - if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_MISSILE)) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.MissileSpec")); - } else { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); - } - } - } - - // Target SPAs - if (te != null) { - // Shaky Stick - Target gets a +1 bonus against Ground-to-Air attacks - if (te.hasAbility(OptionsConstants.PILOT_SHAKY_STICK) - && (te.isAirborne() || te.isAirborneVTOLorWIGE()) - && !ae.isAirborne() && !ae.isAirborneVTOLorWIGE()) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.ShakyStick")); - } - // Urban Guerrilla - Target gets a +1 bonus in any sort of urban terrain - if (te.hasAbility(OptionsConstants.INFANTRY_URBAN_GUERRILLA) - && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.PAVEMENT) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.ROAD) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.RUBBLE) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.BUILDING) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.ROUGH))) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.UrbanGuerilla")); - } - // Forest Ranger - Target gets a +1 bonus in wooded terrain when moving at walking speed or greater - if (te.hasAbility(OptionsConstants.PILOT_TM_FOREST_RANGER) - && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.WOODS) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.JUNGLE)) - && te.moved == EntityMovementType.MOVE_WALK) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.ForestRanger")); - } - // Swamp Beast - Target gets a +1 bonus in mud/swamp terrain when running/flanking - if (te.hasAbility(OptionsConstants.PILOT_TM_SWAMP_BEAST) - && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.MUD) - || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.SWAMP)) - && te.moved == EntityMovementType.MOVE_RUN) { - toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SwampBeast")); - } - - } return toHit; } @@ -4135,6 +3937,7 @@ private static ToHitData compileCrewToHitMods(Game game, Entity ae, Entity te, T * Convenience method that compiles the ToHit modifiers applicable to the defender's condition and actions * -4 for shooting at an immobile target? You'll find that here. * Attacker strafing? Using a weapon with a TH penalty? Those are in other methods. + * For simplicity's sake, Quirks and SPAs now get applied here for general cases (elsewhere for Artillery or ADA) * * @param game The current {@link Game} * @param ae The Entity making this attack @@ -4462,14 +4265,12 @@ else if ((atype != null) } } - // blood stalker SPA - if (ae.getBloodStalkerTarget() > Entity.NONE) { - if (ae.getBloodStalkerTarget() == target.getId()) { - toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BloodStalkerTarget")); - } else { - toHit.addModifier(+2, Messages.getString("WeaponAttackAction.BloodStalkerNonTarget")); - } - } + // Quirks + processAttackerQuirks(toHit, ae, te, weapon); + + // SPAs + processAttackerSPAs(toHit, ae, te, weapon, game); + processDefenderSPAs(toHit, ae, te, weapon, game); return toHit; } @@ -5057,6 +4858,15 @@ private static ToHitData artilleryDirectToHit(Game game, Entity ae, Targetable t // Per TO:AR 6th printing, p153, other mods are: Flak (-2), AMM, damage, intervening trees/jungle int distance = Compute.effectiveDistance(game, ae, target); toHit = new ToHitData(ae.getCrew().getGunnery(), Messages.getString("WeaponAttackAction.GunSkill")); + + /// Re-apply quirks and SPAs after wiping out toHit + // Quirks + processAttackerQuirks(toHit, ae, te, weapon); + + // SPAs + processAttackerSPAs(toHit, ae, te, weapon, game); + processDefenderSPAs(toHit, ae, te, weapon, game); + // Flak; ADA won't hit the later artillery flak check so add this modifier directly. toHit.addModifier(-2, Messages.getString("WeaponAttackAction.Flak")); // AMM @@ -5221,6 +5031,13 @@ private static ToHitData handleArtilleryAttacks(Game game, Entity ae, Targetable toHit = new ToHitData(); } + // Quirks + processAttackerQuirks(toHit, ae, te, weapon); + + // SPAs + processAttackerSPAs(toHit, ae, te, weapon, game); + processDefenderSPAs(toHit, ae, te, weapon, game); + //Homing warheads just need a flat 4 to seek out a successful TAG if (isHoming) { srt.setSpecialResolution(true); @@ -5248,6 +5065,234 @@ private static ToHitData handleArtilleryAttacks(Game game, Entity ae, Targetable return toHit; } + + public static ToHitData processAttackerQuirks(ToHitData toHit, Entity ae, Targetable target, Mounted weapon){ + + // Anti-air targeting quirk vs airborne unit + if (ae.hasQuirk(OptionsConstants.QUIRK_POS_ANTI_AIR) && (target instanceof Entity)) { + if (target.isAirborneVTOLorWIGE() || target.isAirborne()) { + toHit.addModifier(-2, Messages.getString("WeaponAttackAction.AaVsAir")); + } + } + + // Sensor ghosts quirk + if (ae.hasQuirk(OptionsConstants.QUIRK_NEG_SENSOR_GHOSTS)) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SensorGhosts")); + } + + if (null != weapon) { + + // Flat -1 for Accurate Weapon + if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_ACCURATE)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.AccWeapon")); + } + // Flat +1 for Inaccurate Weapon + if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_INACCURATE)) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.InAccWeapon")); + } + // Stable Weapon - Reduces running/flanking penalty by 1 + if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_POS_STABLE_WEAPON) && (ae.moved == EntityMovementType.MOVE_RUN)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.StableWeapon")); + } + // +1 for a Misrepaired Weapon - See StratOps Partial Repairs + if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_MISREPAIRED)) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.MisrepairedWeapon")); + } + // +1 for a Misreplaced Weapon - See StratOps Partial Repairs + if (weapon.hasQuirk(OptionsConstants.QUIRK_WEAP_NEG_MISREPLACED)) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.MisreplacedWeapon")); + } + } + return toHit; + } + + public static ToHitData processAttackerSPAs(ToHitData toHit, Entity ae, Targetable target, Mounted weapon, Game game){ + + // blood stalker SPA + if (ae.getBloodStalkerTarget() > Entity.NONE) { + if (ae.getBloodStalkerTarget() == target.getId()) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BloodStalkerTarget")); + } else { + toHit.addModifier(+2, Messages.getString("WeaponAttackAction.BloodStalkerNonTarget")); + } + } + + WeaponType wtype = ((weapon != null) && (weapon.getType() instanceof WeaponType)) ? (WeaponType) weapon.getType() : null; + + if (wtype != null) { + // Unofficial weapon class specialist - Does not have an unspecialized penalty + if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_LASER) + && wtype.hasFlag(WeaponType.F_ENERGY)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunLSkill")); + } + + if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_BALLISTIC) + && wtype.hasFlag(WeaponType.F_BALLISTIC)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunBSkill")); + } + + if (ae.hasAbility(OptionsConstants.UNOFF_GUNNERY_MISSILE) + && wtype.hasFlag(WeaponType.F_MISSILE)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.GunMSkill")); + } + + // Is the pilot a weapon specialist? + 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 (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 + if (wtype.hasFlag(WeaponType.F_ENERGY)) { + if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_ENERGY)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.EnergySpec")); + } else { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); + } + } else if (wtype.hasFlag(WeaponType.F_BALLISTIC)) { + if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_BALLISTIC)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.BallisticSpec")); + } else { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); + } + } else if (wtype.hasFlag(WeaponType.F_MISSILE)) { + if (ae.hasAbility(OptionsConstants.GUNNERY_SPECIALIST, Crew.SPECIAL_MISSILE)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.MissileSpec")); + } else { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.Unspec")); + } + } + } + + // SPA Environmental Specialist + // Could be pattern-matching instanceof in Java 17 + if (target instanceof Entity) { + Entity te = (Entity) target; + + // Fog Specialist + if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_FOG) + && wtype.hasFlag(WeaponType.F_ENERGY) && !game.getBoard().inSpace() + && (game.getPlanetaryConditions().getFog() == PlanetaryConditions.FOG_HEAVY)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.FogSpec")); + } + + // Light Specialist + if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_LIGHT)) { + if (!te.isIlluminated() + && ((game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_DUSK) + || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_FULL_MOON) + || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_MOONLESS) + || (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_PITCH_BLACK))) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.LightSpec")); + } else if (te.isIlluminated() + && (game.getPlanetaryConditions().getLight() == PlanetaryConditions.L_PITCH_BLACK)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.LightSpec")); + } + } + + // Rain Specialist + if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_RAIN)) { + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_LIGHT_RAIN) + && ae.isConventionalInfantry()) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.RainSpec")); + } + + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_MOD_RAIN) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_HEAVY_RAIN) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_GUSTING_RAIN) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_DOWNPOUR)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.RainSpec")); + } + } + + // Snow Specialist + if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_SNOW)) { + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_LIGHT_SNOW) + && ae.isConventionalInfantry()) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); + } + + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_ICE_STORM) + && wtype.hasFlag(WeaponType.F_MISSILE)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); + } + + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_SLEET) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_SNOW_FLURRIES) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_MOD_SNOW) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WE_HEAVY_SNOW)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); + } + } + + // Wind Specialist + if (ae.getCrew().getOptions().stringOption(OptionsConstants.MISC_ENV_SPECIALIST).equals(Crew.ENVSPC_WIND)) { + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_MOD_GALE) + && wtype.hasFlag(WeaponType.F_MISSILE)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.SnowSpec")); + } + + if (wtype.hasFlag(WeaponType.F_MISSILE) && wtype.hasFlag(WeaponType.F_BALLISTIC) + && ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_STRONG_GALE) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_STORM))) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.WindSpec")); + } + + if ((game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_TORNADO_F13) + || (game.getPlanetaryConditions().getWeather() == PlanetaryConditions.WI_TORNADO_F4)) { + toHit.addModifier(-1, Messages.getString("WeaponAttackAction.WindSpec")); + } + } + } + } + + return toHit; + } + + public static ToHitData processDefenderSPAs(ToHitData toHit, Entity ae, Entity te, Mounted weapon, Game game){ + + if (null == te) { + return toHit; + } + + // Shaky Stick - Target gets a +1 bonus against Ground-to-Air attacks + if (te.hasAbility(OptionsConstants.PILOT_SHAKY_STICK) + && (te.isAirborne() || te.isAirborneVTOLorWIGE()) + && !ae.isAirborne() && !ae.isAirborneVTOLorWIGE()) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.ShakyStick")); + } + // Urban Guerrilla - Target gets a +1 bonus in any sort of urban terrain + if (te.hasAbility(OptionsConstants.INFANTRY_URBAN_GUERRILLA) + && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.PAVEMENT) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.ROAD) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.RUBBLE) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.BUILDING) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.ROUGH))) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.UrbanGuerilla")); + } + // Forest Ranger - Target gets a +1 bonus in wooded terrain when moving at walking speed or greater + if (te.hasAbility(OptionsConstants.PILOT_TM_FOREST_RANGER) + && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.WOODS) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.JUNGLE)) + && te.moved == EntityMovementType.MOVE_WALK) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.ForestRanger")); + } + // Swamp Beast - Target gets a +1 bonus in mud/swamp terrain when running/flanking + if (te.hasAbility(OptionsConstants.PILOT_TM_SWAMP_BEAST) + && (game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.MUD) + || game.getBoard().getHex(te.getPosition()).containsTerrain(Terrains.SWAMP)) + && te.moved == EntityMovementType.MOVE_RUN) { + toHit.addModifier(+1, Messages.getString("WeaponAttackAction.SwampBeast")); + } + + return toHit; + } + @Override public String toDisplayableString(Client client) { return "attacking " + getTarget(client.getGame()).getDisplayName() + " with " + getEntity(client.getGame()).getEquipment(weaponId).getName();