package games.strategy.triplea.delegate; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Route; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.sound.SoundPath; import games.strategy.triplea.attachments.TechAbilityAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.player.ITripleAPlayer; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.Match; /** * Code to fire AA guns while in combat and non combat move. */ class AAInMoveUtil implements Serializable { private static final long serialVersionUID = 1787497998642717678L; private transient IDelegateBridge m_bridge; private transient PlayerID m_player; private Collection<Unit> m_casualties = new ArrayList<>(); private final ExecutionStack m_executionStack = new ExecutionStack(); AAInMoveUtil() {} public AAInMoveUtil initialize(final IDelegateBridge bridge) { m_bridge = bridge; m_player = bridge.getPlayerID(); return this; } private GameData getData() { return m_bridge.getData(); } private boolean isAlwaysONAAEnabled() { return games.strategy.triplea.Properties.getAlwaysOnAA(getData()); } private boolean isAATerritoryRestricted() { return games.strategy.triplea.Properties.getAATerritoryRestricted(getData()); } private ITripleAPlayer getRemotePlayer(final PlayerID id) { return (ITripleAPlayer) m_bridge.getRemotePlayer(id); } private ITripleAPlayer getRemotePlayer() { return getRemotePlayer(m_player); } /** * Fire aa guns. Returns units to remove. */ Collection<Unit> fireAA(final Route route, final Collection<Unit> units, final Comparator<Unit> decreasingMovement, final UndoableMove currentMove) { if (m_executionStack.isEmpty()) { populateExecutionStack(route, units, decreasingMovement, currentMove); } m_executionStack.execute(m_bridge); return m_casualties; } private void populateExecutionStack(final Route route, final Collection<Unit> units, final Comparator<Unit> decreasingMovement, final UndoableMove currentMove) { final List<Unit> targets = new ArrayList<>(units); // select units with lowest movement first targets.sort(decreasingMovement); final List<IExecutable> executables = new ArrayList<>(); for (Territory location : getTerritoriesWhereAAWillFire(route, units)) { executables.add(new IExecutable() { private static final long serialVersionUID = -1545771595683434276L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireAA(location, targets, currentMove); } }); } Collections.reverse(executables); m_executionStack.push(executables); } Collection<Territory> getTerritoriesWhereAAWillFire(final Route route, final Collection<Unit> units) { final boolean alwaysOnAA = isAlwaysONAAEnabled(); // Just the attacked territory will have AA firing if (!alwaysOnAA && isAATerritoryRestricted()) { return Collections.emptyList(); } final GameData data = getData(); // No AA in nonCombat unless 'Always on AA' if (GameStepPropertiesHelper.isNonCombatMove(data, false) && !alwaysOnAA) { return Collections.emptyList(); } // can't rely on m_player being the unit owner in Edit Mode // look at the units being moved to determine allies and enemies final PlayerID movingPlayer = movingPlayer(units); final HashMap<String, HashSet<UnitType>> airborneTechTargetsAllowed = TechAbilityAttachment.getAirborneTargettedByAA(movingPlayer, data); // don't iterate over the end // that will be a battle // and handled else where in this tangled mess final Match<Unit> hasAA = Matches.UnitIsAAthatCanFire(units, airborneTechTargetsAllowed, movingPlayer, Matches.UnitIsAAforFlyOverOnly, 1, true, data); // AA guns in transports shouldn't be able to fire final List<Territory> territoriesWhereAAWillFire = new ArrayList<>(); for (final Territory current : route.getMiddleSteps()) { if (current.getUnits().someMatch(hasAA)) { territoriesWhereAAWillFire.add(current); } } if (games.strategy.triplea.Properties.getForceAAattacksForLastStepOfFlyOver(data)) { if (route.getEnd().getUnits().someMatch(hasAA)) { territoriesWhereAAWillFire.add(route.getEnd()); } } else { // Since we are not firing on the last step, check the start as well, to prevent the user from moving to and from // AA sites one at a // time // if there was a battle fought there then don't fire, this covers the case where we fight, and "Always On AA" // wants to fire after the // battle. // TODO: there is a bug in which if you move an air unit to a battle site in the middle of non combat, it wont // fire if (route.getStart().getUnits().someMatch(hasAA) && !getBattleTracker().wasBattleFought(route.getStart())) { territoriesWhereAAWillFire.add(route.getStart()); } } return territoriesWhereAAWillFire; } private BattleTracker getBattleTracker() { return DelegateFinder.battleDelegate(getData()).getBattleTracker(); } private PlayerID movingPlayer(final Collection<Unit> units) { if (Match.someMatch(units, Matches.unitIsOwnedBy(m_player))) { return m_player; } if (units != null) { for (final Unit u : units) { if (u != null && u.getOwner() != null) { return u.getOwner(); } } } return PlayerID.NULL_PLAYERID; } /** * Fire the aa units in the given territory, hits are removed from units. */ private void fireAA(final Territory territory, final Collection<Unit> units, final UndoableMove currentMove) { if (units.isEmpty()) { return; } final PlayerID movingPlayer = movingPlayer(units); final HashMap<String, HashSet<UnitType>> airborneTechTargetsAllowed = TechAbilityAttachment.getAirborneTargettedByAA(movingPlayer, getData()); final List<Unit> defendingAA = territory.getUnits().getMatches(Matches.UnitIsAAthatCanFire(units, airborneTechTargetsAllowed, movingPlayer, Matches.UnitIsAAforFlyOverOnly, 1, true, getData())); // comes ordered alphabetically already final List<String> AAtypes = UnitAttachment.getAllOfTypeAAs(defendingAA); // stacks are backwards Collections.reverse(AAtypes); for (final String currentTypeAA : AAtypes) { final Collection<Unit> currentPossibleAA = Match.getMatches(defendingAA, Matches.UnitIsAAofTypeAA(currentTypeAA)); final Set<UnitType> targetUnitTypesForThisTypeAA = UnitAttachment.get(currentPossibleAA.iterator().next().getType()).getTargetsAA(getData()); final Set<UnitType> airborneTypesTargettedToo = airborneTechTargetsAllowed.get(currentTypeAA); final Collection<Unit> validTargetedUnitsForThisRoll = Match.getMatches(units, new CompositeMatchOr<>(Matches.unitIsOfTypes(targetUnitTypesForThisTypeAA), new CompositeMatchAnd<Unit>(Matches.UnitIsAirborne, Matches.unitIsOfTypes(airborneTypesTargettedToo)))); // once we fire the AA guns, we can't undo // otherwise you could keep undoing and redoing // until you got the roll you wanted currentMove.setCantUndo("Move cannot be undone after " + currentTypeAA + " has fired."); final DiceRoll[] dice = new DiceRoll[1]; final IExecutable rollDice = new IExecutable() { private static final long serialVersionUID = 4714364489659654758L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { // get rid of units already killed, so we don't target them twice validTargetedUnitsForThisRoll.removeAll(m_casualties); if (!validTargetedUnitsForThisRoll.isEmpty()) { dice[0] = DiceRoll.rollAA(validTargetedUnitsForThisRoll, currentPossibleAA, m_bridge, territory, true); } } }; final IExecutable selectCasualties = new IExecutable() { private static final long serialVersionUID = -8633263235214834617L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validTargetedUnitsForThisRoll.isEmpty()) { final int hitCount = dice[0].getHits(); if (hitCount == 0) { if (currentTypeAA.equals("AA")) { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_MISS, findDefender(currentPossibleAA, territory)); } else { m_bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_MISS, findDefender(currentPossibleAA, territory)); } getRemotePlayer().reportMessage("No " + currentTypeAA + " hits in " + territory.getName(), "No " + currentTypeAA + " hits in " + territory.getName()); } else { if (currentTypeAA.equals("AA")) { m_bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_HIT, findDefender(currentPossibleAA, territory)); } else { m_bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_HIT, findDefender(currentPossibleAA, territory)); } selectCasualties(dice[0], units, validTargetedUnitsForThisRoll, currentPossibleAA, defendingAA, territory, currentTypeAA); } } } }; // push in reverse order of execution m_executionStack.push(selectCasualties); m_executionStack.push(rollDice); } } private PlayerID findDefender(final Collection<Unit> defendingUnits, final Territory territory) { if (defendingUnits == null || defendingUnits.isEmpty()) { if (territory != null && territory.getOwner() != null && !territory.getOwner().isNull()) { return territory.getOwner(); } return PlayerID.NULL_PLAYERID; } else if (territory != null && territory.getOwner() != null && !territory.getOwner().isNull() && Match.someMatch(defendingUnits, Matches.unitIsOwnedBy(territory.getOwner()))) { return territory.getOwner(); } for (final Unit u : defendingUnits) { if (u != null && u.getOwner() != null) { return u.getOwner(); } } return PlayerID.NULL_PLAYERID; } /** * hits are removed from units. Note that units are removed in the order * that the iterator will move through them. */ private void selectCasualties(final DiceRoll dice, final Collection<Unit> allFriendlyUnits, final Collection<Unit> validTargetedUnitsForThisRoll, final Collection<Unit> defendingAA, final Collection<Unit> allEnemyUnits, final Territory territory, final String currentTypeAA) { final CasualtyDetails casualties = BattleCalculator.getAACasualties(false, validTargetedUnitsForThisRoll, allFriendlyUnits, defendingAA, allEnemyUnits, dice, m_bridge, territory.getOwner(), m_player, null, territory, TerritoryEffectHelper.getEffects(territory), false, new ArrayList<>()); getRemotePlayer().reportMessage(casualties.size() + " " + currentTypeAA + " hits in " + territory.getName(), casualties.size() + " " + currentTypeAA + " hits in " + territory.getName()); BattleDelegate.markDamaged(new ArrayList<>(casualties.getDamaged()), m_bridge); m_bridge.getHistoryWriter().addChildToEvent( MyFormatter.unitsToTextNoOwner(casualties.getKilled()) + " lost in " + territory.getName(), new ArrayList<>(casualties.getKilled())); allFriendlyUnits.removeAll(casualties.getKilled()); if (m_casualties == null) { m_casualties = new ArrayList<>(casualties.getKilled()); } else { m_casualties.addAll(casualties.getKilled()); } } }