diff --git a/megamek/src/megamek/client/bot/princess/FireControl.java b/megamek/src/megamek/client/bot/princess/FireControl.java index 76d1981f5d7..b81209b4d57 100644 --- a/megamek/src/megamek/client/bot/princess/FireControl.java +++ b/megamek/src/megamek/client/bot/princess/FireControl.java @@ -299,7 +299,7 @@ ToHitData guessToHitModifierHelperForAnyAttack(final Entity shooter, final int smokeLevel = targetHex.terrainLevel(Terrains.SMOKE); if (1 <= smokeLevel) { - // Smoke level doesn't necessary correspond to the to-hit modifier + // Smoke level doesn't necessarily correspond to the to-hit modifier // even levels are light smoke, odd are heavy smoke toHitData.addModifier((smokeLevel % 2) + 1, TH_SMOKE); } diff --git a/megamek/src/megamek/client/bot/princess/Princess.java b/megamek/src/megamek/client/bot/princess/Princess.java index 2db59fa761c..2ba123212c5 100644 --- a/megamek/src/megamek/client/bot/princess/Princess.java +++ b/megamek/src/megamek/client/bot/princess/Princess.java @@ -56,7 +56,7 @@ public class Princess extends BotClient { private final IHonorUtil honorUtil = new HonorUtil(); private boolean initialized = false; - + // path rankers and fire controls, organized by their explicitly given types to avoid confusion private HashMap pathRankers; private HashMap fireControls; @@ -64,9 +64,9 @@ public class Princess extends BotClient { private FireControlState fireControlState; private PathRankerState pathRankerState; private ArtilleryTargetingControl atc; - + private Integer spinupThreshold = null; - + private BehaviorSettings behaviorSettings; private double moveEvaluationTimeEstimate = 0; private final Precognition precognition; @@ -75,7 +75,8 @@ public class Princess extends BotClient { * Mapping to hold the damage allocated to each targetable, stored by ID. * Used to allocate damage more intelligently and avoid overkill. */ - private final ConcurrentHashMap damageMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap damageMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap, ArrayList> incomingGuidablesMap = new ConcurrentHashMap<>(); private final Set strategicBuildingTargets = new HashSet<>(); private boolean fallBack = false; private final ChatProcessor chatProcessor = new ChatProcessor(); @@ -84,9 +85,9 @@ public class Princess extends BotClient { private final Set attackedWhileFleeing = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set crippledUnits = new HashSet<>(); - /** + /** * Returns a new Princess Bot with the given behavior and name, configured for the given - * host and port. The new Princess Bot outputs its settings to its own logger. + * host and port. The new Princess Bot outputs its settings to its own logger. */ public static Princess createPrincess(String name, String host, int port, BehaviorSettings behavior) { Princess result = new Princess(name, host, port); @@ -104,10 +105,10 @@ public static Princess createPrincess(String name, String host, int port, Behavi public Princess(final String name, final String host, final int port) { super(name, host, port); setBehaviorSettings(BehaviorSettingsFactory.getInstance().DEFAULT_BEHAVIOR); - + fireControlState = new FireControlState(); pathRankerState = new PathRankerState(); - + // Start-up precog now, so that it can instantiate its game instance, // and it will stay up-to date. precognition = new Precognition(this); @@ -123,7 +124,7 @@ public ArtilleryTargetingControl getArtilleryTargetingControl() { if (atc == null) { atc = new ArtilleryTargetingControl(); } - + return atc; } @@ -140,10 +141,10 @@ IPathRanker getPathRanker(Entity entity) { } else if (entity.isAero() && game.useVectorMove()) { return pathRankers.get(PathRankerType.NewtonianAerospace); } - + return pathRankers.get(PathRankerType.Basic); } - + IPathRanker getPathRanker(PathRankerType pathRankerType) { return pathRankers.get(pathRankerType); } @@ -155,7 +156,7 @@ public boolean getFallBack() { boolean getFleeBoard() { return fleeBoard; } - + /** * Picks a tag target based on the data contained within the given GameCFREvent * Expects the event to have some tag targets and tag target types. @@ -165,12 +166,12 @@ protected int pickTagTarget(GameCFREvent evt) { List TAGTargets = evt.getTAGTargets(); List TAGTargetTypes = evt.getTAGTargetTypes(); Map tagTargetHexes = new HashMap<>(); // maps coordinates to target index - + // Algorithm: // get a list of the hexes being tagged // figure out how much damage a hit to each of the tagged hexes will do (relatively) // pick the one which will result in the best damage - + // get list of targetable hexes for (int tagIndex = 0; tagIndex < TAGTargets.size(); tagIndex++) { int nType = TAGTargetTypes.get(tagIndex); @@ -179,20 +180,20 @@ protected int pickTagTarget(GameCFREvent evt) { tagTargetHexes.put(tgt.getPosition(), tagIndex); } } - + Entity arbitraryEntity = getArbitraryEntity(); if (arbitraryEntity == null) { return 0; } - + double maxDamage = -Double.MAX_VALUE; Coords maxDamageHex = null; - + // invoke ArtilleryTargetingControl.calculateDamageValue for (Coords targetHex : tagTargetHexes.keySet()) { // a note on parameters: // we don't care about exact damage value since we're just comparing them relative to one another - // note: technically we should, + // note: technically we should, // we don't care about specific firing entity, we just want one on our side // since we only use it to determine friendliness double currentDamage = getArtilleryTargetingControl().calculateDamageValue(10, targetHex, arbitraryEntity, game, this); @@ -201,7 +202,7 @@ protected int pickTagTarget(GameCFREvent evt) { maxDamageHex = targetHex; } } - + if (maxDamageHex != null) { return tagTargetHexes.get(maxDamageHex); } else { @@ -222,22 +223,22 @@ private void setFleeBoard(final boolean fleeBoard, final String reason) { public FireControlState getFireControlState() { return fireControlState; } - + public PathRankerState getPathRankerState() { return pathRankerState; } - + Precognition getPrecognition() { return precognition; } - + public int getMaxWeaponRange(Entity entity) { return getMaxWeaponRange(entity, false); } - + /** * Retrieves maximum weapon range for the given entity. - * Cached version of entity.getMaxWeaponRange() + * Cached version of entity.getMaxWeaponRange() * @param entity Entity we're checking * @param airborneTarget Whether the potential target is in the air, only relevant for * aircraft shooting at other aircraft on ground maps. @@ -276,13 +277,13 @@ public void setBehaviorSettings(final BehaviorSettings behaviorSettings) { } final String x = targetCoords.substring(0, 2); final String y = targetCoords.replaceFirst(x, ""); - // Need to subtract 1, since we are given a Hex number string, + // Need to subtract 1, since we are given a Hex number string, // which is Coords X + 1Y + 1 final Coords coords = new Coords(Integer.parseInt(x) - 1, Integer.parseInt(y) - 1); getStrategicBuildingTargets().add(coords); } - + spinupThreshold = null; } @@ -302,14 +303,14 @@ FireControl getFireControl(Entity entity) { entity.getCrew().getCrewType().getMaxPrimaryTargets() < 0) { return fireControls.get(FireControlType.MultiTarget); } - + return fireControls.get(FireControlType.Basic); } - + FireControl getFireControl(FireControlType fireControlType) { - return fireControls.get(fireControlType); + return fireControls.get(fireControlType); } - + public UnitBehavior getUnitBehaviorTracker() { return unitBehaviorTracker; } @@ -344,7 +345,7 @@ public void addStrategicBuildingTarget(final Coords coords) { public Set getPriorityUnitTargets() { return getBehaviorSettings().getPriorityUnitTargets(); } - + public Targetable getAppropriateTarget(Coords strategicTarget) { if (null == game.getBoard().getBuildingAt(strategicTarget)) { return new HexTarget(strategicTarget, Targetable.TYPE_HEX_CLEAR); @@ -366,9 +367,11 @@ protected Vector calculateArtyAutoHitHexes() { return new PlayerIDandList<>(); } } - + @Override protected void initTargeting() { + // Reset incoming guided weapon lists for each friendly unit. + incomingGuidablesMap.clear(); getArtilleryTargetingControl().initializeForTargetingPhase(); } @@ -386,7 +389,7 @@ protected void calculateDeployment() { sendDeleteEntity(entityNum); return; } - + // get a list of all coordinates to which we can deploy final List startingCoords = getStartingCoordsArray(game.getEntity(entityNum)); if (startingCoords.isEmpty()) { @@ -396,17 +399,17 @@ protected void calculateDeployment() { // get the coordinates I can deploy on final Coords deployCoords = getFirstValidCoords(getEntity(entityNum), startingCoords); if (null == deployCoords) { - // if I cannot deploy anywhere, then I get rid of the entity instead so that we may go about our business + // if I cannot deploy anywhere, then I get rid of the entity instead so that we may go about our business LogManager.getLogger().error("getCoordsAround gave no location for " + getEntity(entityNum).getChassis() + ". Removing unit."); - + sendDeleteEntity(entityNum); return; } // first coordinate that it is legal to put this unit on now find some sort of reasonable // facing. If there are deployed enemies, face them - + // specifically, face the last deployed enemy. int decentFacing = -1; for (final Entity e : getEnemyEntities()) { @@ -416,7 +419,7 @@ protected void calculateDeployment() { } } - // if I haven't found a decent facing, then at least face towards + // if I haven't found a decent facing, then at least face towards // the center of the board if (-1 == decentFacing) { final Coords center = new Coords(game.getBoard().getWidth() / 2, @@ -433,7 +436,7 @@ protected void calculateDeployment() { deployElevation -= deployHex.getLevel(); deploy(entityNum, deployCoords, decentFacing, deployElevation); } - + /** * Calculate the deployment elevation for the given entity. * Gun Emplacements should deploy on the rooftop of the building for maximum visibility. @@ -460,7 +463,7 @@ private int getDeployElevation(Entity deployEntity, Hex deployHex) { if (!validCoords.isEmpty()) { return validCoords.get(0); } - + return null; } else if (getGame().useVectorMove()) { return calculateAdvancedAerospaceDeploymentCoords(deployedUnit, possibleDeployCoords); @@ -468,9 +471,9 @@ private int getDeployElevation(Entity deployEntity, Hex deployHex) { return super.getFirstValidCoords(deployedUnit, possibleDeployCoords); } } - + /** - * Function that calculates deployment coordinates + * Function that calculates deployment coordinates * @param deployedUnit The unit being considered for deployment * @param possibleDeployCoords The coordinates being considered for deployment * @return The first valid deployment coordinates. @@ -482,16 +485,16 @@ private Coords calculateAdvancedAerospaceDeploymentCoords(final Entity deployedU return coords; } } - + // if we can't find any good deployment coordinates, deploy anyway to the first available one // and maybe eventually we'll slow down enough that we can deploy without immediately flying off if (!possibleDeployCoords.isEmpty()) { return possibleDeployCoords.get(0); } - + return null; } - + /** * Helper function that calculates the possible locations where a given gun emplacement can be deployed * @param deployedUnit The unit to check @@ -511,7 +514,7 @@ private List calculateTurretDeploymentLocations(final GunEmplacement dep if (null != building) { final int buildingHeight = hex.terrainLevel(Terrains.BLDG_ELEV); - + // check stacking violation at the roof level final Entity violation = Compute.stackingViolation(game, deployedUnit, coords, buildingHeight, coords, null, deployedUnit.climbMode()); @@ -525,7 +528,7 @@ private List calculateTurretDeploymentLocations(final GunEmplacement dep turretDeploymentLocations.sort((arg0, arg1) -> calculateTurretDeploymentValue(arg1) - calculateTurretDeploymentValue(arg0)); return turretDeploymentLocations; } - + /** * Helper function that calculates the "utility" of placing a turret at the given coords * @param coords The location of the building being considered. @@ -534,7 +537,7 @@ private List calculateTurretDeploymentLocations(final GunEmplacement dep private int calculateTurretDeploymentValue(final Coords coords) { // algorithm: a building is valued by the following formula: // (CF + height * 2) / # turrets placed on the roof - // This way, we will generally favor unpopulated higher CF buildings, + // This way, we will generally favor unpopulated higher CF buildings, // but have some wiggle room in case of a really tall high CF building final Building building = game.getBoard().getBuildingAt(coords); final Hex hex = game.getBoard().getHex(coords); @@ -542,18 +545,18 @@ private int calculateTurretDeploymentValue(final Coords coords) { return (building.getCurrentCF(coords) + hex.terrainLevel(Terrains.BLDG_ELEV) * 2) / turretCount; } - + @Override protected void calculateFiringTurn() { try { - // get the first entity that can act this turn make sure weapons + // get the first entity that can act this turn make sure weapons // are loaded final Entity shooter = getEntityToFire(fireControlState); - // Forego firing if - // a) hidden, - // b) under "peaceful" forced withdrawal, - // c) majority firepower is jammed + // Forego firing if + // a) hidden, + // b) under "peaceful" forced withdrawal, + // c) majority firepower is jammed // d) best firing plan comes up as crap (no expected damage/null) // // If foregoing firing, unjam highest-damage weapons first, then turret @@ -583,7 +586,7 @@ protected void calculateFiringTurn() { LogManager.getLogger().info("Hidden unit skips firing."); } - // calculating a firing plan is somewhat expensive, so + // calculating a firing plan is somewhat expensive, so // we skip this step if we have already decided not to fire due to being hidden or under "peaceful forced withdrawal" if (!skipFiring) { // Set up ammo conservation. @@ -599,7 +602,7 @@ protected void calculateFiringTurn() { LogManager.getLogger().info(shooter.getDisplayName() + " - Best Firing Plan: " + plan.getDebugDescription(LogManager.getLogger().getLevel().isLessSpecificThan(Level.DEBUG))); - // Add expected damage from the chosen FiringPlan to the + // Add expected damage from the chosen FiringPlan to the // damageMap for the target enemy. // while we're looping through all the shots anyway, send any firing mode changes for (WeaponFireInfo shot : plan) { @@ -623,7 +626,7 @@ protected void calculateFiringTurn() { } actions.addAll(plan.getEntityActionVector()); - + EntityAction spotAction = getFireControl(shooter).getSpotAction(plan, shooter, fireControlState); if (spotAction != null) { actions.add(spotAction); @@ -768,7 +771,7 @@ private Map calcAmmoConservation(final Entity shooter) { /** * Worker method that calculates a point blank shot action vector given a firing entity ID and a target ID. - * + * * @param firingEntityID the ID of the entity taking the point blank shot * @param targetID the ID of the entity being shot at potentially */ @@ -781,13 +784,13 @@ protected Vector calculatePointBlankShot(int firingEntityID, int t } final FiringPlanCalculationParameters fccp = new Builder().buildExact(shooter, target, calcAmmoConservation(shooter)); - FiringPlan plan = getFireControl(shooter).determineBestFiringPlan(fccp); + FiringPlan plan = getFireControl(shooter).determineBestFiringPlan(fccp); getFireControl(shooter).loadAmmo(shooter, plan); plan.sortPlan(); return plan.getEntityActionVector(); } - + @Override protected Vector calculateMinefieldDeployment() { try { @@ -831,7 +834,7 @@ protected Vector calculateMinefieldDeployment() { // Get the distance to the nearest enemy. final double distance = getPathRanker(entity).distanceToClosestEnemy(entity, entity.getPosition(), game); - + msg.append("\n\t\tDistance to Nearest Enemy: ").append(numberFormat.format(distance)); // Get the ratio of distance to speed. @@ -912,7 +915,7 @@ Entity getEntityToFire(FireControlState fireControlState) { fireControlState.getOrderedFiringEntities().removeFirst(); return entityToReturn; } - + /** * Sorts firing entities to ensure that entities that can do indirect fire go after * entities that cannot, so that IDF units go after spotting units. @@ -934,14 +937,14 @@ private void initFiringEntities(FireControlState fireControlState) { } } } - + /** - * Loops through the list of entities controlled by this Princess instance + * Loops through the list of entities controlled by this Princess instance * and decides which should be moved first. * Immobile units and ejected MechWarriors / crews will be moved first. - * After that, each unit is given an index// This unit should have already - * moved due to the isImmobilized check. via the - * {@link #calculateMoveIndex(Entity, StringBuilder)} method. The highest + * After that, each unit is given an index// This unit should have already + * moved due to the isImmobilized check. via the + * {@link #calculateMoveIndex(Entity, StringBuilder)} method. The highest * index value is moved first. * * @return The entity that should be moved next. @@ -955,12 +958,12 @@ Entity getEntityToMove() { final StringBuilder msg = new StringBuilder("Deciding who to move next."); for (final Entity entity : myEntities) { msg.append("\n\tUnit ").append(entity.getDisplayName()); - + if (entity.isDone()) { msg.append("has already moved this phase"); continue; } - + if (!getGame().getPhase().isSimultaneous(getGame()) && (entity.isOffBoard() || (null == entity.getPosition()) || entity.isUnloadedThisTurn() @@ -975,13 +978,13 @@ Entity getEntityToMove() { movingEntity = entity; break; } - + if (entity instanceof MechWarrior) { msg.append("is ejected crew."); movingEntity = entity; break; } - + // can't do anything with out-of-control aeros, so use them as init sinks if (entity.isAero() && ((IAero) entity).isOutControlTotal()) { msg.append("is out-of-control aero."); @@ -1031,7 +1034,7 @@ Entity getEntityToMove() { // get the first entity that can act this turn final Entity attacker = game.getFirstEntity(getMyTurn()); - // If my unit is forced to withdraw, don't attack unless I've been + // If my unit is forced to withdraw, don't attack unless I've been // attacked. if (getForcedWithdrawal() && attacker.isCrippled()) { final StringBuilder msg = new StringBuilder(attacker.getDisplayName()) @@ -1078,7 +1081,7 @@ boolean isFallingBack(final Entity entity) { boolean canShootWhileFallingBack(Entity entity) { return attackedWhileFleeing.contains(entity.getId()); } - + boolean mustFleeBoard(final Entity entity) { if (!isFallingBack(entity)) { return false; @@ -1112,7 +1115,7 @@ boolean isImmobilized(final Entity mover) { final MovePath movePath = new MovePath(getGame(), mover); - // For a normal fall-shame setting (index 5), our threshold should be + // For a normal fall-shame setting (index 5), our threshold should be // a 10+ piloting roll. final int threshold; switch (getBehaviorSettings().getFallShameIndex()) { @@ -1151,7 +1154,7 @@ boolean isImmobilized(final Entity mover) { ? MoveStepType.CAREFUL_STAND : MoveStepType.GET_UP; final MoveStep getUp = new MoveStep(movePath, type); - // If our odds to get up are equal to or worse than the threshold, + // If our odds to get up are equal to or worse than the threshold, // consider ourselves immobile. final PilotingRollData target = mech.checkGetUp(getUp, movePath.getLastStepMovementType()); LogManager.getLogger().info("Need to roll " + target.getValue() + @@ -1198,7 +1201,7 @@ protected MovePath continueMovementFor(final Entity entity) { LogManager.getLogger().debug(msg); sendChat(msg, Level.ERROR); - // If this entity is falling back, able to flee the board, on + // If this entity is falling back, able to flee the board, on // its home edge, and must flee, do so. if (mustFleeBoard(entity)) { final MovePath mp = new MovePath(game, entity); @@ -1216,7 +1219,7 @@ protected MovePath continueMovementFor(final Entity entity) { return mp; } } - + final List paths = getMovePathsAndSetNecessaryTargets(entity, false); if (null == paths) { @@ -1241,7 +1244,7 @@ protected MovePath continueMovementFor(final Entity entity) { getPathRanker(entity).initUnitTurn(entity, getGame()); // fall tolerance range between 0.50 and 1.0 final double fallTolerance = getBehaviorSettings().getFallShameIndex() / 20d + 0.50d; - + final List rankedpaths = getPathRanker(entity).rankPaths(paths, getGame(), getMaxWeaponRange(entity), fallTolerance, getEnemyEntities(), getFriendEntities()); @@ -1254,9 +1257,9 @@ protected MovePath continueMovementFor(final Entity entity) { if (0 == moveEvaluationTimeEstimate) { moveEvaluationTimeEstimate = updatedEstimate; } - + moveEvaluationTimeEstimate = 0.5 * (updatedEstimate + moveEvaluationTimeEstimate); - + if (rankedpaths.isEmpty()) { return performPathPostProcessing(new MovePath(game, entity), 0); } @@ -1316,7 +1319,7 @@ protected void initFiring() { // the damageMap for allocating damage during firing. // Reset the map generated during the movement phase- The available - // targets may have changed during that time(ejections, enemies + // targets may have changed during that time(ejections, enemies // fleeing, etc). damageMap.clear(); // Now add an ID for each possible target. @@ -1331,7 +1334,7 @@ protected void initFiring() { } } - + /** * Function with side effects. Retrieves the move path collection we want * the entity to consider. Sometimes it's the standard "circle", sometimes it's pruned long-range movement paths @@ -1341,20 +1344,20 @@ public List getMovePathsAndSetNecessaryTargets(Entity mover, boolean f if (mover.isImmobile()) { return Collections.emptyList(); } - + BehaviorType behavior = forceMoveToContact ? BehaviorType.MoveToContact : unitBehaviorTracker.getBehaviorType(mover, this); // during the movement phase, it is technically necessary to clear this data between each unit - // as the state of the board may have changed due to crashes etc. + // as the state of the board may have changed due to crashes etc. // generating movable clusters is a relatively cheap operation, so it's not a big deal getClusterTracker().clearMovableAreas(); getClusterTracker().updateMovableAreas(mover); - - // basic idea: + + // basic idea: // if we're "in battle", just use the standard set of move paths // if we're trying to get somewhere // - sort all long range paths by "mp cost" (actual MP + how long it'll take to do terrain leveling) // - set the first terrain/building as 'strategic target' if the shortest path requires terrain leveling - // - if the first strategic target is in LOS at the pruned end of the shortest path, + // - if the first strategic target is in LOS at the pruned end of the shortest path, // then we actually return the paths for "engaged" behavior // - if we're unable to get where we're going, use standard set of move paths switch (behavior) { @@ -1388,7 +1391,7 @@ public List getMovePathsAndSetNecessaryTargets(Entity mover, boolean f getFireControlState().getAdditionalTargets().add(levelingTarget); sendChat("Hex " + levelingTarget.getPosition().toFriendlyString() + " impedes route to destination, targeting for clearing.", Level.INFO); } - + // if any of the long range paths, pruned, are within LOS of leveling coordinates, then we're actually // just going to go back to the standard unit paths List prunedPaths = new ArrayList<>(); @@ -1429,7 +1432,7 @@ private void checkForDishonoredEnemies() { final StringBuilder msg = new StringBuilder("Checking for dishonored enemies."); try { - // If the Forced Withdrawal rule is not turned on, then it's a + // If the Forced Withdrawal rule is not turned on, then it's a // fight to the death anyway. if (!getForcedWithdrawal()) { msg.append("\n\tForced withdrawal turned off."); @@ -1445,7 +1448,7 @@ private void checkForDishonoredEnemies() { // Is my unit trying to withdraw as per forced withdrawal rules? // shortcut: we already check for forced withdrawal above, so need to do that here - final boolean fleeing = crippledUnits.contains(mine.getId()); + final boolean fleeing = crippledUnits.contains(mine.getId()); for (final int id : attackedBy) { final Entity entity = getGame().getEntity(id); @@ -1455,7 +1458,7 @@ private void checkForDishonoredEnemies() { if (getHonorUtil().isEnemyBroken(entity.getId(), entity.getOwnerId(), getForcedWithdrawal()) || !entity.isMilitary()) { - // If he'd just continued running, I would have let him + // If he'd just continued running, I would have let him // go, but the bastard shot at me! msg.append("\n\t") .append(entity.getDisplayName()) @@ -1487,7 +1490,7 @@ private void checkForDishonoredEnemies() { } private void checkForBrokenEnemies() { - // If the Forced Withdrawal rule is not turned on, then it's a fight + // If the Forced Withdrawal rule is not turned on, then it's a fight // to the death anyway. if (!getForcedWithdrawal()) { return; @@ -1497,7 +1500,7 @@ private void checkForBrokenEnemies() { getHonorUtil().checkEnemyBroken(entity, getForcedWithdrawal()); } } - + /** * Update the various state trackers for a specific entity. * Useful to call when receiving an entity update packet @@ -1543,7 +1546,7 @@ protected void initMovement() { final Coords coords = bldgCoords.nextElement(); for (final Entity entity : game.getEntitiesVector(coords, true)) { final Targetable bt = getAppropriateTarget(coords); - + if (isEnemyGunEmplacement(entity, coords)) { fireControlState.getAdditionalTargets().add(bt); sendChat("Building in Hex " + coords.toFriendlyString() @@ -1556,11 +1559,12 @@ protected void initMovement() { // Next, collect the ID's of each potential target and store them in // the damageMap for allocating damage during movement. // Right now, this doesn't get filled because I can't find where - // FiringPlans for potential move paths are calculated(pretty sure + // FiringPlans for potential move paths are calculated(pretty sure // they are, though). This needs to be fixed at some point. // Reset last round's damageMap damageMap.clear(); + // Now add an ID for each possible target. final List potentialTargets = FireControl.getAllTargetableEnemyEntities( getLocalPlayer(), getGame(), fireControlState); @@ -1587,7 +1591,7 @@ public void initialize() { checkForDishonoredEnemies(); checkForBrokenEnemies(); refreshCrippledUnits(); - + initializePathRankers(); fireControlState = new FireControlState(); pathRankerState = new PathRankerState(); @@ -1617,7 +1621,7 @@ public void initialize() { } } - + /** * Initialize the fire controls. */ @@ -1626,10 +1630,10 @@ public void initializeFireControls() { FireControl fireControl = new FireControl(this); fireControls.put(FireControlType.Basic, fireControl); - + InfantryFireControl infantryFireControl = new InfantryFireControl(this); fireControls.put(FireControlType.Infantry, infantryFireControl); - + MultiTargetFireControl multiTargetFireControl = new MultiTargetFireControl(this); fireControls.put(FireControlType.MultiTarget, multiTargetFireControl); } @@ -1640,22 +1644,93 @@ public void initializeFireControls() { */ public void initializePathRankers() { initializeFireControls(); - + pathRankers = new HashMap<>(); BasicPathRanker basicPathRanker = new BasicPathRanker(this); basicPathRanker.setPathEnumerator(precognition.getPathEnumerator()); pathRankers.put(PathRankerType.Basic, basicPathRanker); - + InfantryPathRanker infantryPathRanker = new InfantryPathRanker(this); infantryPathRanker.setPathEnumerator(precognition.getPathEnumerator()); pathRankers.put(PathRankerType.Infantry, infantryPathRanker); - + NewtonianAerospacePathRanker newtonianAerospacePathRanker = new NewtonianAerospacePathRanker(this); newtonianAerospacePathRanker.setPathEnumerator(precognition.getPathEnumerator()); pathRankers.put(PathRankerType.NewtonianAerospace, newtonianAerospacePathRanker); } - + + /** + * Because Aerospace units can either TAG _or_ attack with their weapons, we want every + * friendly TAG-equipped Aero (and possibly others) to know about all the possible + * incoming Homing or IF attacks that could take advantage of their TAGs, for later calculations. + * + * Steps are: + * 1. All relevant guidable attacks (within range of the target) are added to a list. + * 1a. All Homing Weapons landing this turn are added to the list. + * 1b. All IF-capable, Semi-Guided, and Homing weapons that have not yet fired but _could_ are added as well. + * 2. Each attacker-target location pair's expected homing/indirect fire damage is cached for later re-use. + * 3. During the Indirect phase, when TAG attacks are announced, relevant units can use this info to decide + * whether they want to TAG or reserve their activation for actual attacks. + * 4. All info is cleared at the start of the next turn. + */ + public ArrayList computeGuidedWeapons(Entity entityToFire, Coords location){ + // Key cached entries to firing entity _and_ coordinates, as each shooter may have different targets and weapons + // to support. + Map.Entry key = Map.entry(entityToFire.getId(), location); + if (incomingGuidablesMap.containsKey(key)) { + return incomingGuidablesMap.get(key); + } + + ArrayList friendlyGuidedWeapons = new ArrayList(); + + // First, friendly incoming homing artillery that will land this turn. May include entity's own shots from prior + // turns, but not from this turn. + for (Enumeration attacks = game.getArtilleryAttacks(); attacks.hasMoreElements(); ) { + ArtilleryAttackAction a = attacks.nextElement(); + if (a.getTurnsTilHit() == 0 && a.getAmmoMunitionType().contains(AmmoType.Munitions.M_HOMING) && (!a.getEntity(game).isEnemyOf(entityToFire))) { + // Must be a shot that should land within 8 hexes of the target hex + if (a.getCoords().distance(location) <= 8) { + friendlyGuidedWeapons.add(a.getEntity(game).getEquipment(a.getWeaponId())); + } + } + } + + // Next find all friendly Homing-weapon-havers, Indirect-Firers, and LG-Bombers within range. + for (Entity f : new HashSet(getEntitiesOwned())) { + if (f.equals(entityToFire)) { + continue; // This entity's weapons should not be considered for this calculation + } + Set candidateWeapons = new HashSet(); + Coords fLoc = f.getPosition(); + + for (Mounted m : f.getTotalWeaponList()) { + WeaponType w = (WeaponType) m.getType(); + // Ignore weapons outside of viable long range; does not apply to bombs. + if (fLoc.distance(location) > w.getLongRange() + f.getRunMP() && !m.isGroundBomb()) { + continue; + } + if (w.hasIndirectFire() && !f.isAero()) { + // Only care about ground IF weapons. + candidateWeapons.add(m); + } else if (m.getLinked() != null && m.getLinked().isHomingAmmoInHomingMode()) { + candidateWeapons.add(m); + } else if (m.isGroundBomb()) { + // Only care about Laser-Guided bombs here; Homing Arrow IV handled separately. + if (((IBomber) f).getBombs().stream().anyMatch(b -> ((BombType) b.getType()).getBombType() == BombType.B_LG)) { + candidateWeapons.add(m); + } + } + } + + // All candidate weapons should be unique instances. + friendlyGuidedWeapons.addAll(candidateWeapons); + } + // Cache result in case needed for later pathing / planning. + incomingGuidablesMap.put(key, friendlyGuidedWeapons); + return friendlyGuidedWeapons; + } + /** * Load the list of units considered crippled at the time the bot was loaded or the beginning of the turn, * whichever is the more recent. @@ -1665,18 +1740,18 @@ public void refreshCrippledUnits() { if (!getForcedWithdrawal()) { return; } - + // this approach is a little bit inefficient, but the running time is only O(n) where n is the number - // of princess owned units, so it shouldn't be a big deal. + // of princess owned units, so it shouldn't be a big deal. crippledUnits.clear(); - + for (Entity e : this.getEntitiesOwned()) { if (e.isCrippled(true)) { crippledUnits.add(e.getId()); } } } - + private boolean isEnemyGunEmplacement(final Entity entity, final Coords coords) { return entity.hasETypeFlag(Entity.ETYPE_GUN_EMPLACEMENT) @@ -1708,24 +1783,24 @@ public synchronized void die() { protected void processChat(final GamePlayerChatEvent ge) { chatProcessor.processChat(ge, this); } - + /** * Given an entity and the current behavior settings, get the "home" edge to which the entity should attempt to retreat * Guaranteed to return a cardinal edge or NONE. */ CardinalEdge getHomeEdge(Entity entity) { - // if I am crippled and using forced withdrawal rules, my home edge is the "retreat" edge + // if I am crippled and using forced withdrawal rules, my home edge is the "retreat" edge if (entity.isCrippled(true) && getBehaviorSettings().isForcedWithdrawal()) { if (getBehaviorSettings().getRetreatEdge() == CardinalEdge.NEAREST) { - return BoardUtilities.getClosestEdge(entity); + return BoardUtilities.getClosestEdge(entity); } else { return getBehaviorSettings().getRetreatEdge(); } } - + // otherwise, return the destination edge if (getBehaviorSettings().getDestinationEdge() == CardinalEdge.NEAREST) { - return BoardUtilities.getClosestEdge(entity); + return BoardUtilities.getClosestEdge(entity); } else { return getBehaviorSettings().getDestinationEdge(); } @@ -1760,7 +1835,7 @@ protected void checkMorale() { IHonorUtil getHonorUtil() { return honorUtil; } - + /** * Lazy-loaded calculation of the "to-hit target number" threshold for * spinning up a rapid fire autocannon. @@ -1771,10 +1846,10 @@ public int getSpinupThreshold() { // dropping it down to 6+ TN at the lower aggression levels spinupThreshold = Math.min(11, Math.max(getBehaviorSettings().getHyperAggressionIndex() + 2, 6)); } - + return spinupThreshold; } - + public void resetSpinupThreshold() { spinupThreshold = null; } @@ -1799,7 +1874,7 @@ protected void handlePacket(final Packet c) { LogManager.getLogger().trace(msg.toString()); } } - + /** * sends a load game file to the server */ @@ -1808,11 +1883,11 @@ public void sendLoadGame(final File f) { precognition.resetGame(); super.sendLoadGame(f); } - + public void sendPrincessSettings() { send(new Packet(PacketCommand.PRINCESS_SETTINGS, behaviorSettings)); } - + @Override protected void disconnected() { if (null != precognition) { @@ -1834,17 +1909,17 @@ int getHighestEnemyInitiativeId() { } return highestEnemyInitiativeId; } - + /** * Helper function to perform some modifications to a given path. - * Intended to happen after we pick the best path. + * Intended to happen after we pick the best path. * @param path The ranked path to process * @return Altered move path */ private MovePath performPathPostProcessing(final RankedPath path) { return performPathPostProcessing(path.getPath(), path.getExpectedDamage()); } - + /** * Helper function to perform some modifications to a given path. * @param path The move path to process @@ -1858,40 +1933,40 @@ private MovePath performPathPostProcessing(MovePath path, double expectedDamage) unloadTransportedInfantry(retval); launchFighters(retval); unjamRAC(retval); - + // if we are using vector movement, there's a whole bunch of post-processing that happens to // aircraft flight paths when a player does it, so we apply it here. if (path.getEntity().isAero() || (path.getEntity() instanceof EjectedCrew && path.getEntity().isSpaceborne())) { retval = SharedUtility.moveAero(retval, null); } - + return retval; } - + /** * Helper function that appends an unjam RAC command to the end of a qualifying path. * @param path The path to process. */ - private void unjamRAC(MovePath path) { + private void unjamRAC(MovePath path) { if (path.getEntity().canUnjamRAC() && (path.getMpUsed() <= path.getEntity().getWalkMP()) && !path.isJumping()) { path.addStep(MoveStepType.UNJAM_RAC); } } - + /** * Helper function that insinuates an "evade" step for aircraft that will not be shooting. * @param path The path to process */ private void evadeIfNotFiring(MovePath path, boolean possibleToInflictDamage) { Entity pathEntity = path.getEntity(); - + // we cannot evade if we are out of control if (pathEntity.isAero() && pathEntity.isAirborne() && ((IAero) pathEntity).isOutControlTotal()) { return; } - + // if we're an airborne aircraft // and we're not going to do any damage anyway // and we can do so without causing a PSR @@ -1902,7 +1977,7 @@ private void evadeIfNotFiring(MovePath path, boolean possibleToInflictDamage) { path.addStep(MoveStepType.EVADE); } } - + /** * Turn on the searchlight if we expect to be shooting at something and it's dark out * @param path Path being considered @@ -1911,17 +1986,17 @@ private void evadeIfNotFiring(MovePath path, boolean possibleToInflictDamage) { private void turnOnSearchLight(MovePath path, boolean possibleToInflictDamage) { Entity pathEntity = path.getEntity(); if (possibleToInflictDamage && - pathEntity.hasSearchlight() && + pathEntity.hasSearchlight() && !pathEntity.isUsingSearchlight() && (path.getGame().getPlanetaryConditions().getLight() >= PlanetaryConditions.L_FULL_MOON)) { path.addStep(MoveStepType.SEARCHLIGHT); } } - + /** * Helper function that adds an "unload" step for units that are transporting infantry * if the conditions for unloading are favorable. - * + * * Infantry unloading logic is different from, for example, hot-dropping mechs or launching aerospace fighters, * so we handle it separately. * @param path The path to modify @@ -1951,43 +2026,43 @@ private void unloadTransportedInfantry(MovePath path) { if (transport instanceof Bay) { continue; } - + for (Entity loadedEntity : transport.getLoadedUnits()) { // there's really no good reason for Princess to disconnect trailers. // Let's skip those for now. We don't want to create a bogus 'unload' step for them anyhow. if (loadedEntity.isTrailer() && loadedEntity.getTowedBy() != Entity.NONE) { continue; } - // favorable conditions include: + // favorable conditions include: // - the loaded entity should be able to enter the current terrain // - the loaded entity should be within max weapons range + movement range of an enemy // - unloading the loaded entity cannot violate stacking limits - // - only one unit - + // - only one unit + // this condition is a simple check that we're not unloading infantry into deep space // or into lava or some other such nonsense boolean unloadFatal = loadedEntity.isBoardProhibited(getGame().getBoard().getType()) || loadedEntity.isLocationProhibited(pathEndpoint) || loadedEntity.isLocationDeadly(pathEndpoint); - + // Unloading a unit may sometimes cause a stacking violation, take that into account when planning boolean unloadIllegal = Compute.stackingViolation(getGame(), loadedEntity, pathEndpoint, movingEntity, loadedEntity.climbMode()) != null; - + // this is a primitive condition that checks whether we're within "engagement range" of an enemy // where "engagement range" is defined as the maximum range of our weapons plus our walking movement boolean inEngagementRange = loadedEntity.getWalkMP() + getMaxWeaponRange(loadedEntity) >= distanceToClosestEnemy; - + if (!unloadFatal && !unloadIllegal && inEngagementRange) { path.addStep(MoveStepType.UNLOAD, loadedEntity, pathEndpoint); - return; // we can only unload one infantry unit per hex per turn, so once we've unloaded, we're done. + return; // we can only unload one infantry unit per hex per turn, so once we've unloaded, we're done. } } } } - + /** - * Helper function that adds an "launch" step for units that are transporting + * Helper function that adds an "launch" step for units that are transporting * launchable units in some kind of bay. */ private void launchFighters(MovePath path) { @@ -1996,7 +2071,7 @@ private void launchFighters(MovePath path) { if (getBehaviorSettings().shouldAutoFlee()) { return; } - + Entity movingEntity = path.getEntity(); Coords pathEndpoint = path.getFinalCoords(); Targetable closestEnemy = getPathRanker(movingEntity).findClosestEnemy(movingEntity, pathEndpoint, getGame(), false); @@ -2005,20 +2080,20 @@ private void launchFighters(MovePath path) { if ((null == closestEnemy) || (closestEnemy.getTargetType() != Targetable.TYPE_ENTITY)) { return; } - + TreeMap> unitsToLaunch = new TreeMap<>(); boolean executeLaunch = false; - + // loop through all fighter (or smallcraft) bays in the current entity // grouping launched craft by bay to limit launches to 'safe' rate. Vector fighterBays = movingEntity.getFighterBays(); - + for (int bayIndex = 0; bayIndex < fighterBays.size(); bayIndex++) { Bay bay = fighterBays.get(bayIndex); - + for (Entity loadedEntity : bay.getLaunchableUnits()) { unitsToLaunch.putIfAbsent(bayIndex, new Vector<>()); - + // for now, just launch fighters at the 'safe' rate if (unitsToLaunch.get(bayIndex).size() < bay.getSafeLaunchRate()) { unitsToLaunch.get(bayIndex).add(loadedEntity.getId()); @@ -2028,7 +2103,7 @@ private void launchFighters(MovePath path) { } } } - + // only add the step if we're actually launching something if (executeLaunch) { path.addStep(MoveStepType.LAUNCH, unitsToLaunch); @@ -2045,7 +2120,7 @@ public void sendChat(final String message, final Level logLevel) { * Override for the 'receive entity update' handler * Updates internal state in addition to base client functionality */ - @Override + @Override public void receiveEntityUpdate(final Packet packet) { super.receiveEntityUpdate(packet); updateEntityState((Entity) packet.getObject(1)); diff --git a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java index e0382971585..c2ca8fbb861 100644 --- a/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java +++ b/megamek/src/megamek/client/bot/princess/WeaponFireInfo.java @@ -26,9 +26,7 @@ import java.text.DecimalFormat; import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; +import java.util.*; /** * WeaponFireInfo is a wrapper around a WeaponAttackAction that includes @@ -412,6 +410,12 @@ private WeaponAttackAction buildBombAttackAction(final HashMap bo return 1; } + // Aero TAG usage needs to take into account incoming Indirect Fire shots from friendlies, homing + // weapons, and the disutility of foregoing its own other weapons. + if (weapon.getEntity().isAero() && weaponType.hasFlag(WeaponType.F_TAG) && !target.isAero()){ + return computeAeroExpectedTAGDamage(); + } + if (getTarget() instanceof Entity) { double dmg = Compute.getExpectedDamage(getGame(), getAction(), true, owner.getPrecognition().getECMInfo()); @@ -424,6 +428,21 @@ private WeaponAttackAction buildBombAttackAction(final HashMap bo return weaponType.getDamage(); } + /** + * Aerospace units need to think carefully before firing TAGs at ground targets, because this + * precludes firing _any_ other weapons this turn. + * @return expected damage of firing a TAG weapon, in light of other options. + */ + double computeAeroExpectedTAGDamage(){ + int myWeaponsDamage = Compute.computeTotalDamage(shooter.getTotalWeaponList()); + + // Get a list of incoming or potentially incoming guidable weapons from the relevant Princess and compute damage. + int incomingAttacksDamage = Compute.computeTotalDamage(owner.computeGuidedWeapons(shooter, target.getPosition())); + + // If TAG damage exceeds the attacking unit's own max damage capacity, go for it! + return Math.max(incomingAttacksDamage - myWeaponsDamage, 0); + } + /** * Compute the heat output by firing a given weapon. * Contains special logic for bay weapons when using individual bay heat. @@ -572,6 +591,20 @@ void initDamage(@Nullable final MovePath shooterPath, msg.append("\n\tMax Damage: ").append(LOG_DEC.format(maxDamage)); } + // If expected damage from Aero tagging is zero, return out - save attacks for later. + if (weapon.getType().hasFlag(WeaponType.F_TAG) && shooter.isAero() && getExpectedDamageOnHit() <= 0) { + if (debugging) { + LogManager.getLogger().debug(msg.append("\n\tAerospace TAG attack not advised at this juncture")); + } + setProbabilityToHit(0); + setMaxDamage(0); + setHeat(0); + setExpectedCriticals(0); + setKillProbability(0); + setExpectedDamageOnHit(0); + return; + } + final double expectedCriticalHitCount = ProbabilityCalculator.getExpectedCriticalHitCount(); // there's always the chance of rolling a '2' diff --git a/megamek/src/megamek/common/Compute.java b/megamek/src/megamek/common/Compute.java index 684e38bf8b3..e4d1f49205d 100644 --- a/megamek/src/megamek/common/Compute.java +++ b/megamek/src/megamek/common/Compute.java @@ -26,6 +26,7 @@ import megamek.common.weapons.InfantryAttack; import megamek.common.weapons.Weapon; import megamek.common.weapons.artillery.ArtilleryCannonWeapon; +import megamek.common.weapons.battlearmor.ISBAPopUpMineLauncher; import megamek.common.weapons.bayweapons.BayWeapon; import megamek.common.weapons.gaussrifles.HAGWeapon; import megamek.common.weapons.infantry.InfantryWeapon; @@ -1105,19 +1106,7 @@ public static ToHitData getRangeMods(Game game, Entity ae, int weaponId, boolean isAttackerBA = (ae instanceof BattleArmor); boolean isWeaponInfantry = (wtype instanceof InfantryWeapon) && !wtype.hasFlag(WeaponType.F_TAG); boolean isSwarmOrLegAttack = (wtype instanceof InfantryAttack); - boolean isIndirect = ((wtype.getAmmoType() == AmmoType.T_LRM) - || (wtype.getAmmoType() == AmmoType.T_LRM_IMP) - || (wtype.getAmmoType() == AmmoType.T_MML) - || (wtype.getAmmoType() == AmmoType.T_EXLRM) - || (wtype.getAmmoType() == AmmoType.T_TBOLT_5) - || (wtype.getAmmoType() == AmmoType.T_TBOLT_10) - || (wtype.getAmmoType() == AmmoType.T_TBOLT_15) - || (wtype.getAmmoType() == AmmoType.T_TBOLT_20) - || (wtype.getAmmoType() == AmmoType.T_IATM) - || (wtype.getAmmoType() == AmmoType.T_LRM_TORPEDO) - || (wtype.getAmmoType() == AmmoType.T_MEK_MORTAR) - || (wtype instanceof ArtilleryCannonWeapon)) - && weapon.curMode().equals("Indirect"); + boolean isIndirect = wtype.hasIndirectFire() && weapon.curMode().equals("Indirect"); boolean useExtremeRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_RANGE); boolean useLOSRange = game.getOptions().booleanOption(OptionsConstants.ADVCOMBAT_TACOPS_LOS_RANGE); //Naval C3 only provides full C3 range benefits to energy weapons and guided missiles @@ -6487,6 +6476,37 @@ public static int directBlowInfantryDamage(double damage, int mos, isAttackThruBuilding, attackerId, vReport, 1); } + /** + * @return the maximum damage that a set of weapons can generate. + */ + public static int computeTotalDamage(List weaponList){ + int totalDmg = 0; + for (Mounted weapon : weaponList) { + if (!weapon.isBombMounted() && weapon.isCrippled()) { + continue; + } + WeaponType type = (WeaponType) weapon.getType(); + if (type.getDamage() == WeaponType.DAMAGE_VARIABLE) { + // Estimate rather than compute exact bay / trooper damage sum. + totalDmg += type.getRackSize(); + } else if (type.getDamage() == WeaponType.DAMAGE_ARTILLERY + || type.getDamage() == WeaponType.DAMAGE_BY_CLUSTERTABLE) { + totalDmg += type.getRackSize(); + } else if (type.getDamage() == WeaponType.DAMAGE_SPECIAL) {// Handle dive bomb attacks here! + if (type instanceof DiveBombAttack) { + totalDmg += weapon.getEntity().bombList.stream().mapToInt(Mounted::getExplosionDamage).sum(); + } + if (type instanceof ISBAPopUpMineLauncher) { + totalDmg += 4; + } + } else { + totalDmg += type.getDamage(); + } + } + + return totalDmg; + } + /** * Method replicates the Non-Conventional Damage against Infantry damage * table as well as shifting for direct blows. also adjust for non-infantry diff --git a/megamek/src/megamek/common/Entity.java b/megamek/src/megamek/common/Entity.java index 68ccab5bf4f..93ef23cfbfb 100644 --- a/megamek/src/megamek/common/Entity.java +++ b/megamek/src/megamek/common/Entity.java @@ -814,6 +814,12 @@ public abstract class Entity extends TurnOrdered implements Transporter, Targeta private UnitRole role = UnitRole.UNDETERMINED; + /** + * Vector storing references to friendly weapon attack actions this entity may need to support; + * Primarily used by Princess to speed up TAG utility calculations. + */ + protected ArrayList incomingGuidedAttacks; + /** * Generates a new, blank, entity. */ @@ -852,6 +858,7 @@ public Entity() { externalId = UUID.randomUUID().toString(); initTechAdvancement(); offBoardShotObservers = new HashSet<>(); + incomingGuidedAttacks = new ArrayList(); } /** @@ -868,7 +875,9 @@ protected void initMilitary() { } protected boolean hasViableWeapons() { - int totalDmg = 0; + int totalDmg = Compute.computeTotalDamage(getTotalWeaponList()); + + // Find any weapons with range of 6+ boolean hasRangeSixPlus = false; List weaponList = getTotalWeaponList(); for (Mounted weapon : weaponList) { @@ -876,22 +885,9 @@ protected boolean hasViableWeapons() { continue; } WeaponType type = (WeaponType) weapon.getType(); - if (type.getDamage() == WeaponType.DAMAGE_VARIABLE) { - - } else if (type.getDamage() == WeaponType.DAMAGE_ARTILLERY) { - return true; - } else if (type.getDamage() == WeaponType.DAMAGE_BY_CLUSTERTABLE) { - totalDmg += type.getRackSize(); - } else if (type.getDamage() == WeaponType.DAMAGE_SPECIAL) { - if (type instanceof ISBAPopUpMineLauncher) { - totalDmg += 4; - } - } else { - totalDmg += type.getDamage(); - } - if (type.getLongRange() >= 6) { hasRangeSixPlus = true; + break; } } return (totalDmg >= 5) || hasRangeSixPlus; @@ -6324,6 +6320,13 @@ public void newRound(int roundNumber) { resetBombAttacks(); } + // Reset deployed or incoming weapons which need TAG guidance (mainly for bot computations) + if (incomingGuidedAttacks == null) { + incomingGuidedAttacks = new ArrayList(); + } else { + incomingGuidedAttacks.clear(); + } + // reset evasion setEvading(false); @@ -12041,6 +12044,20 @@ public int getSensorCheck() { return sensorCheck; } + /** + * @param attacksList of know or possible WAAs that might need TAG assistance this turn. + */ + public void setIncomingGuidedAttacks(ArrayList attacksList){ + incomingGuidedAttacks = (ArrayList) attacksList.clone(); + } + + /** + * @return the vector of possible WAAs that may need this entity's TAG support. + */ + public ArrayList getIncomingGuidedAttacks(){ + return incomingGuidedAttacks; + } + /** * A method to determine if an aero has suffered 3 sensor hits. * When double-blind is on, this affects both standard visibility and sensor rolls diff --git a/megamek/src/megamek/common/WeaponType.java b/megamek/src/megamek/common/WeaponType.java index b8585228f0a..ceeb894e28b 100644 --- a/megamek/src/megamek/common/WeaponType.java +++ b/megamek/src/megamek/common/WeaponType.java @@ -308,6 +308,13 @@ public class WeaponType extends EquipmentType { protected boolean subCapital = false; protected int atClass = CLASS_NONE; + /** + * @return true if the wtype is able to be fired indirectly. + */ + public boolean canIndirect() { + return false; + } + public void setDamage(int inD) { damage = inD; } diff --git a/megamek/src/megamek/common/weapons/artillery/ArtilleryCannonWeapon.java b/megamek/src/megamek/common/weapons/artillery/ArtilleryCannonWeapon.java index db41122aec0..1cdd2799f57 100644 --- a/megamek/src/megamek/common/weapons/artillery/ArtilleryCannonWeapon.java +++ b/megamek/src/megamek/common/weapons/artillery/ArtilleryCannonWeapon.java @@ -37,9 +37,19 @@ public ArtilleryCannonWeapon() { atClass = CLASS_AC; } + @Override + public boolean isAlphaStrikeIndirectFire() { + return false; + } + + @Override + public boolean hasIndirectFire() { + return true; + } + /* * (non-Javadoc) - * + * * @see * megamek.common.weapons.Weapon#getCorrectHandler(megamek.common.ToHitData, * megamek.common.actions.WeaponAttackAction, megamek.common.Game, @@ -52,7 +62,7 @@ protected AttackHandler getCorrectHandler(ToHitData toHit, // game.getEntity(waa.getEntityId()).getEquipment(waa.getWeaponId()).getLinked().getType(); return new ArtilleryCannonWeaponHandler(toHit, waa, game, manager); } - + @Override public void adaptToGameOptions(GameOptions gOp) { super.adaptToGameOptions(gOp); diff --git a/megamek/src/megamek/common/weapons/artillery/ArtilleryWeapon.java b/megamek/src/megamek/common/weapons/artillery/ArtilleryWeapon.java index 8e5fa43da85..eae8649518c 100644 --- a/megamek/src/megamek/common/weapons/artillery/ArtilleryWeapon.java +++ b/megamek/src/megamek/common/weapons/artillery/ArtilleryWeapon.java @@ -34,9 +34,19 @@ public ArtilleryWeapon() { atClass = CLASS_ARTILLERY; } + @Override + public boolean isAlphaStrikeIndirectFire() { + return false; + } + + @Override + public boolean hasIndirectFire() { + return true; + } + /* * (non-Javadoc) - * + * * @see * megamek.common.weapons.Weapon#getCorrectHandler(megamek.common.ToHitData, * megamek.common.actions.WeaponAttackAction, megamek.common.Game, diff --git a/megamek/src/megamek/common/weapons/lrms/LRTWeapon.java b/megamek/src/megamek/common/weapons/lrms/LRTWeapon.java index 63675c82783..d10b8cd3427 100644 --- a/megamek/src/megamek/common/weapons/lrms/LRTWeapon.java +++ b/megamek/src/megamek/common/weapons/lrms/LRTWeapon.java @@ -50,7 +50,12 @@ public LRTWeapon() { flags = flags.or(F_ARTEMIS_COMPATIBLE); } - + + @Override + public boolean hasIndirectFire() { + return true; + } + @Override public double getTonnage(Entity entity, int location, double size) { if ((entity != null) && entity.hasETypeFlag(Entity.ETYPE_PROTOMECH)) { @@ -65,12 +70,12 @@ protected AttackHandler getCorrectHandler(ToHitData toHit, WeaponAttackAction waa, Game game, GameManager manager) { return new MissileWeaponHandler(toHit, waa, game, manager); } - + @Override public int getBattleForceClass() { return BFCLASS_TORP; } - + @Override public boolean isAlphaStrikeIndirectFire() { return false;