package games.strategy.triplea.delegate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import games.strategy.debug.ClientLogger; import games.strategy.engine.data.Change; import games.strategy.engine.data.CompositeChange; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Route; import games.strategy.engine.data.RouteScripted; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.net.GUID; import games.strategy.sound.SoundPath; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.dataObjects.BattleRecord; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.oddsCalculator.ta.BattleResults; import games.strategy.triplea.ui.display.ITripleADisplay; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.IntegerMap; import games.strategy.util.Match; public class AirBattle extends AbstractBattle { private static final long serialVersionUID = 4686241714027216395L; protected static final String AIR_BATTLE = "Air Battle"; protected static final String INTERCEPTORS_LAUNCH = "Defender Launches Interceptors"; protected static final String ATTACKERS_FIRE = "Attackers Fire"; protected static final String DEFENDERS_FIRE = "Defenders Fire"; protected static final String ATTACKERS_WITHDRAW = "Attackers Withdraw?"; protected static final String DEFENDERS_WITHDRAW = "Defenders Withdraw?"; // protected final static String BOMBERS_TO_TARGETS = "Bombers Fly to Their Targets"; protected final ExecutionStack m_stack = new ExecutionStack(); protected List<String> m_steps; protected final Collection<Unit> m_defendingWaitingToDie = new ArrayList<>(); protected final Collection<Unit> m_attackingWaitingToDie = new ArrayList<>(); protected boolean m_intercept = false; // -1 would mean forever until one side is eliminated. (default is 1 round) protected final int m_maxRounds; public AirBattle(final Territory battleSite, final boolean bombingRaid, final GameData data, final PlayerID attacker, final BattleTracker battleTracker) { super(battleSite, attacker, battleTracker, bombingRaid, (bombingRaid ? BattleType.AIR_RAID : BattleType.AIR_BATTLE), data); m_isAmphibious = false; m_maxRounds = games.strategy.triplea.Properties.getAirBattleRounds(data); updateDefendingUnits(); } protected void updateDefendingUnits() { // fill in defenders if (m_isBombingRun) { m_defendingUnits = m_battleSite.getUnits().getMatches(defendingBombingRaidInterceptors(m_attacker, m_data)); } else { m_defendingUnits = m_battleSite.getUnits().getMatches(defendingGroundSeaBattleInterceptors(m_attacker, m_data)); } } @Override public Change addAttackChange(final Route route, final Collection<Unit> units, final HashMap<Unit, HashSet<Unit>> targets) { m_attackingUnits.addAll(units); return ChangeFactory.EMPTY_CHANGE; } @Override public void removeAttack(final Route route, final Collection<Unit> units) { m_attackingUnits.removeAll(units); } @Override public void fight(final IDelegateBridge bridge) { // remove units that may already be dead due to a previous event (like they died from a strategic bombing raid, // rocket attack, etc) removeUnitsThatNoLongerExist(); // we were interrupted if (m_stack.isExecuting()) { showBattle(bridge); m_stack.execute(bridge); return; } updateDefendingUnits(); bridge.getHistoryWriter().startEvent("Air Battle in " + m_battleSite, m_battleSite); BattleCalculator.sortPreBattle(m_attackingUnits); BattleCalculator.sortPreBattle(m_defendingUnits); m_steps = determineStepStrings(true, bridge); showBattle(bridge); pushFightLoopOnStack(true); m_stack.execute(bridge); } private void pushFightLoopOnStack(final boolean firstRun) { if (m_isOver) { return; } final List<IExecutable> steps = getBattleExecutables(firstRun); // add in the reverse order we create them Collections.reverse(steps); for (final IExecutable step : steps) { m_stack.push(step); } return; } public boolean shouldFightAirBattle() { if (m_isBombingRun) { return Match.someMatch(m_attackingUnits, Matches.UnitIsStrategicBomber) && !m_defendingUnits.isEmpty(); } else { return !m_attackingUnits.isEmpty() && !m_defendingUnits.isEmpty(); } } public boolean shouldEndBattleDueToMaxRounds() { return m_maxRounds > 0 && m_maxRounds <= m_round; } protected boolean canAttackerRetreat() { return !shouldEndBattleDueToMaxRounds() && shouldFightAirBattle() && games.strategy.triplea.Properties.getAirBattleAttackersCanRetreat(m_data); } protected boolean canDefenderRetreat() { return !shouldEndBattleDueToMaxRounds() && shouldFightAirBattle() && games.strategy.triplea.Properties.getAirBattleDefendersCanRetreat(m_data); } List<IExecutable> getBattleExecutables(final boolean firstRun) { final List<IExecutable> steps = new ArrayList<>(); if (shouldFightAirBattle()) { if (firstRun) { steps.add(new InterceptorsLaunch()); } steps.add(new AttackersFire()); steps.add(new DefendersFire()); steps.add(new IExecutable() { // just calculates lost TUV and kills off any suicide units private static final long serialVersionUID = -5575569705493214941L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { // getDisplay(bridge).gotoBattleStep(m_battleID, BOMBERS_TO_TARGETS); if (!m_intercept) { return; } final IntegerMap<UnitType> defenderCosts = BattleCalculator.getCostsForTUV(m_defender, m_data); final IntegerMap<UnitType> attackerCosts = BattleCalculator.getCostsForTUV(m_attacker, m_data); m_attackingUnits.removeAll(m_attackingWaitingToDie); remove(m_attackingWaitingToDie, bridge, m_battleSite); m_defendingUnits.removeAll(m_defendingWaitingToDie); remove(m_defendingWaitingToDie, bridge, m_battleSite); int tuvLostAttacker = BattleCalculator.getTUV(m_attackingWaitingToDie, m_attacker, attackerCosts, m_data); m_attackerLostTUV += tuvLostAttacker; int tuvLostDefender = BattleCalculator.getTUV(m_defendingWaitingToDie, m_defender, defenderCosts, m_data); m_defenderLostTUV += tuvLostDefender; m_attackingWaitingToDie.clear(); m_defendingWaitingToDie.clear(); // kill any suicide attackers (veqryn) final CompositeMatch<Unit> attackerSuicide = new CompositeMatchAnd<>(Matches.UnitIsSuicide); if (m_isBombingRun) { attackerSuicide.add(Matches.UnitIsNotStrategicBomber); } if (Match.someMatch(m_attackingUnits, attackerSuicide)) { final List<Unit> suicideUnits = Match.getMatches(m_attackingUnits, Matches.UnitIsSuicide); m_attackingUnits.removeAll(suicideUnits); remove(suicideUnits, bridge, m_battleSite); tuvLostAttacker = BattleCalculator.getTUV(suicideUnits, m_attacker, attackerCosts, m_data); m_attackerLostTUV += tuvLostAttacker; } if (Match.someMatch(m_defendingUnits, Matches.UnitIsSuicide)) { final List<Unit> suicideUnits = Match.getMatches(m_defendingUnits, Matches.UnitIsSuicide); m_defendingUnits.removeAll(suicideUnits); remove(suicideUnits, bridge, m_battleSite); tuvLostDefender = BattleCalculator.getTUV(suicideUnits, m_defender, defenderCosts, m_data); m_defenderLostTUV += tuvLostDefender; } } }); } steps.add(new IExecutable() { private static final long serialVersionUID = 3148193405425861565L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (shouldFightAirBattle() && !shouldEndBattleDueToMaxRounds()) { return; } makeBattle(bridge); } }); steps.add(new IExecutable() { private static final long serialVersionUID = 3148193405425861565L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (shouldFightAirBattle() && !shouldEndBattleDueToMaxRounds()) { return; } end(bridge); } }); steps.add(new IExecutable() { private static final long serialVersionUID = -5408702756335356985L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver && canAttackerRetreat()) { attackerRetreat(bridge); } } }); steps.add(new IExecutable() { private static final long serialVersionUID = -7819137222487595113L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver && canDefenderRetreat()) { defenderRetreat(bridge); } } }); final IExecutable loop = new IExecutable() { private static final long serialVersionUID = -5408702756335356985L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { pushFightLoopOnStack(false); } }; steps.add(new IExecutable() { private static final long serialVersionUID = -4136481765101946944L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { m_steps = determineStepStrings(false, bridge); final ITripleADisplay display = getDisplay(bridge); display.listBattleSteps(m_battleID, m_steps); m_round++; // continue fighting // the recursive step // this should always be the base of the stack // when we execute the loop, it will populate the stack with the battle steps if (!m_stack.isEmpty()) { throw new IllegalStateException("Stack not empty:" + m_stack); } m_stack.push(loop); } } }); return steps; } public List<String> determineStepStrings(final boolean showFirstRun, final IDelegateBridge bridge) { final List<String> steps = new ArrayList<>(); if (showFirstRun) { steps.add(AIR_BATTLE); steps.add(INTERCEPTORS_LAUNCH); } steps.add(ATTACKERS_FIRE); steps.add(DEFENDERS_FIRE); if (canAttackerRetreat()) { steps.add(ATTACKERS_WITHDRAW); } if (canDefenderRetreat()) { steps.add(DEFENDERS_WITHDRAW); } // steps.add(BOMBERS_TO_TARGETS); return steps; } private static void recordUnitsWereInAirBattle(final Collection<Unit> units, final IDelegateBridge bridge) { final CompositeChange wasInAirBattleChange = new CompositeChange(); for (final Unit u : units) { wasInAirBattleChange.add(ChangeFactory.unitPropertyChange(u, true, TripleAUnit.WAS_IN_AIR_BATTLE)); } if (!wasInAirBattleChange.isEmpty()) { bridge.addChange(wasInAirBattleChange); } } private void makeBattle(final IDelegateBridge bridge) { // record who was in this battle first, so that they do not take part in any ground battles if (m_isBombingRun) { recordUnitsWereInAirBattle(m_attackingUnits, bridge); recordUnitsWereInAirBattle(m_defendingUnits, bridge); } // so as of right now, Air Battles are created before both normal battles and strategic bombing raids // once completed, the air battle will create a strategic bombing raid, if that is the purpose of those aircraft // however, if the purpose is a normal battle, it will have already been created by the battle tracker / combat move // so we do not have to create normal battles, only bombing raids // setup new battle here if (m_isBombingRun) { final Collection<Unit> bombers = Match.getMatches(m_attackingUnits, Matches.UnitIsStrategicBomber); if (!bombers.isEmpty()) { HashMap<Unit, HashSet<Unit>> targets = null; final Collection<Unit> enemyTargetsTotal = m_battleSite.getUnits() .getMatches(new CompositeMatchAnd<>(Matches.enemyUnit(bridge.getPlayerID(), m_data), Matches.UnitIsAtMaxDamageOrNotCanBeDamaged(m_battleSite).invert(), Matches.unitIsBeingTransported().invert())); for (final Unit unit : bombers) { final Collection<Unit> enemyTargets = Match.getMatches(enemyTargetsTotal, Matches.UnitIsLegalBombingTargetBy(unit)); if (!enemyTargets.isEmpty()) { Unit target = null; if (enemyTargets.size() > 1 && games.strategy.triplea.Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(m_data)) { while (target == null) { target = getRemote(bridge).whatShouldBomberBomb(m_battleSite, enemyTargets, Collections.singletonList(unit)); } } else if (!enemyTargets.isEmpty()) { target = enemyTargets.iterator().next(); } if (target != null) { targets = new HashMap<>(); targets.put(target, new HashSet<>(Collections.singleton(unit))); } m_battleTracker.addBattle(new RouteScripted(m_battleSite), Collections.singleton(unit), true, m_attacker, bridge, null, null, targets, true); } } final IBattle battle = m_battleTracker.getPendingBattle(m_battleSite, true, null); final IBattle dependent = m_battleTracker.getPendingBattle(m_battleSite, false, BattleType.NORMAL); if (dependent != null) { m_battleTracker.addDependency(dependent, battle); } final IBattle dependentAirBattle = m_battleTracker.getPendingBattle(m_battleSite, false, BattleType.AIR_BATTLE); if (dependentAirBattle != null) { m_battleTracker.addDependency(dependentAirBattle, battle); } } } } private void end(final IDelegateBridge bridge) { // record it String text; if (!m_attackingUnits.isEmpty()) { if (m_isBombingRun) { if (Match.someMatch(m_attackingUnits, Matches.UnitIsStrategicBomber)) { m_whoWon = WhoWon.ATTACKER; if (m_defendingUnits.isEmpty()) { m_battleResultDescription = BattleRecord.BattleResultDescription.WON_WITHOUT_CONQUERING; } else { m_battleResultDescription = BattleRecord.BattleResultDescription.WON_WITH_ENEMY_LEFT; } text = "Air Battle is over, the remaining bombers go on to their targets"; bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR_SUCCESSFUL, m_attacker); } else { m_whoWon = WhoWon.DRAW; m_battleResultDescription = BattleRecord.BattleResultDescription.STALEMATE; text = "Air Battle is over, the bombers have all died"; bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_FAILURE, m_attacker); } } else { if (m_defendingUnits.isEmpty()) { m_whoWon = WhoWon.ATTACKER; m_battleResultDescription = BattleRecord.BattleResultDescription.WON_WITHOUT_CONQUERING; text = "Air Battle is over, the defenders have all died"; bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR_SUCCESSFUL, m_attacker); } else { m_whoWon = WhoWon.DRAW; m_battleResultDescription = BattleRecord.BattleResultDescription.STALEMATE; text = "Air Battle is over, neither side is eliminated"; bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_STALEMATE, m_attacker); } } } else { m_whoWon = WhoWon.DEFENDER; m_battleResultDescription = BattleRecord.BattleResultDescription.LOST; text = "Air Battle is over, the attackers have all died"; bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_FAILURE, m_attacker); } bridge.getHistoryWriter().addChildToEvent(text); m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); getDisplay(bridge).battleEnd(m_battleID, "Air Battle over"); m_isOver = true; m_battleTracker.removeBattle(AirBattle.this); } public void finishBattleAndRemoveFromTrackerHeadless(final IDelegateBridge bridge) { makeBattle(bridge); m_whoWon = WhoWon.ATTACKER; m_battleResultDescription = BattleRecord.BattleResultDescription.NO_BATTLE; m_battleTracker.getBattleRecords().removeBattle(m_attacker, m_battleID); m_isOver = true; m_battleTracker.removeBattle(AirBattle.this); } private void attackerRetreat(final IDelegateBridge bridge) { // planes retreat to the same square the battle is in, and then should // move during non combat to their landing site, or be scrapped if they can't find one. final Collection<Territory> possible = new ArrayList<>(2); possible.add(m_battleSite); // retreat planes if (!m_attackingUnits.isEmpty()) { queryRetreat(false, bridge, possible); } } private void defenderRetreat(final IDelegateBridge bridge) { // planes retreat to the same square the battle is in, and then should // move during non combat to their landing site, or be scrapped if they can't find one. final Collection<Territory> possible = new ArrayList<>(2); possible.add(m_battleSite); // retreat planes if (!m_defendingUnits.isEmpty()) { queryRetreat(true, bridge, possible); } } private void queryRetreat(final boolean defender, final IDelegateBridge bridge, final Collection<Territory> availableTerritories) { if (availableTerritories.isEmpty()) { return; } final Collection<Unit> units = defender ? new ArrayList<>(m_defendingUnits) : new ArrayList<>(m_attackingUnits); if (units.isEmpty()) { return; } final PlayerID retreatingPlayer = defender ? m_defender : m_attacker; final String text = retreatingPlayer.getName() + " retreat?"; final String step = defender ? DEFENDERS_WITHDRAW : ATTACKERS_WITHDRAW; getDisplay(bridge).gotoBattleStep(m_battleID, step); final Territory retreatTo = getRemote(retreatingPlayer, bridge).retreatQuery(m_battleID, false, m_battleSite, availableTerritories, text); if (retreatTo != null && !availableTerritories.contains(retreatTo)) { System.err.println("Invalid retreat selection :" + retreatTo + " not in " + MyFormatter.defaultNamedToTextList(availableTerritories)); Thread.dumpStack(); return; } if (retreatTo != null) { if (!m_headless) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_AIR, m_attacker); } retreat(units, defender, bridge); final String messageShort = retreatingPlayer.getName() + " retreats"; final String messageLong = retreatingPlayer.getName() + " retreats all units to " + retreatTo.getName(); getDisplay(bridge).notifyRetreat(messageShort, messageLong, step, retreatingPlayer); } } private void retreat(final Collection<Unit> retreating, final boolean defender, final IDelegateBridge bridge) { if (!defender) { // we must remove any of these units from the land battle that follows (this comes before we remove them from this // battle, because // after we remove from this battle we are no longer blocking any battles) final Collection<IBattle> dependentBattles = m_battleTracker.getBlocked(AirBattle.this); removeFromDependents(retreating, bridge, dependentBattles, true); } final String transcriptText = MyFormatter.unitsToText(retreating) + (defender ? " grounded" : " retreated"); final Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; units.removeAll(retreating); bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(retreating)); recordUnitsWereInAirBattle(retreating, bridge); } private void showBattle(final IDelegateBridge bridge) { final String title = "Air Battle in " + m_battleSite.getName(); getDisplay(bridge).showBattle(m_battleID, m_battleSite, title, m_attackingUnits, m_defendingUnits, null, null, null, Collections.emptyMap(), m_attacker, m_defender, isAmphibious(), getBattleType(), Collections.emptySet()); getDisplay(bridge).listBattleSteps(m_battleID, m_steps); } class InterceptorsLaunch implements IExecutable { private static final long serialVersionUID = 4300406315014471768L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { getInterceptors(bridge); if (!m_defendingUnits.isEmpty()) { m_intercept = true; // play a sound bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR, m_attacker); } } private void getInterceptors(final IDelegateBridge bridge) { boolean groundedPlanesRetreated; final Collection<Unit> interceptors; if (m_isBombingRun) { // if bombing run, ask who will intercept interceptors = getRemote(m_defender, bridge).selectUnitsQuery(m_battleSite, new ArrayList<>(m_defendingUnits), "Select Air to Intercept"); groundedPlanesRetreated = false; } else { // if normal battle, we may choose to withdraw some air units (keep them grounded for both Air battle and the // subsequent normal // battle) instead of launching if (games.strategy.triplea.Properties.getAirBattleDefendersCanRetreat(m_data)) { interceptors = getRemote(m_defender, bridge).selectUnitsQuery(m_battleSite, new ArrayList<>(m_defendingUnits), "Select Air to Intercept"); groundedPlanesRetreated = true; } else { // if not allowed to withdraw, we must commit all air interceptors = new ArrayList<>(m_defendingUnits); groundedPlanesRetreated = false; } } if (interceptors != null && !m_defendingUnits.containsAll(interceptors)) { throw new IllegalStateException("Interceptors choose from outside of available units"); } final Collection<Unit> beingRemoved = new ArrayList<>(m_defendingUnits); m_defendingUnits.clear(); if (interceptors != null) { beingRemoved.removeAll(interceptors); m_defendingUnits.addAll(interceptors); } getDisplay(bridge).changedUnitsNotification(m_battleID, m_defender, beingRemoved, null, null); if (groundedPlanesRetreated) { // this removes them from the subsequent normal battle. (do not use this for bombing battles) retreat(beingRemoved, true, bridge); } if (!m_attackingUnits.isEmpty()) { bridge.getHistoryWriter().addChildToEvent(m_attacker.getName() + " attacks with " + m_attackingUnits.size() + " units heading to " + m_battleSite.getName(), new ArrayList<>(m_attackingUnits)); } if (!m_defendingUnits.isEmpty()) { bridge.getHistoryWriter().addChildToEvent(m_defender.getName() + " launches " + m_defendingUnits.size() + " interceptors out of " + m_battleSite.getName(), new ArrayList<>(m_defendingUnits)); } } } class AttackersFire implements IExecutable { private static final long serialVersionUID = -5289634214875797408L; DiceRoll m_dice; CasualtyDetails m_details; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_intercept) { return; } final IExecutable roll = new IExecutable() { private static final long serialVersionUID = 6579019987019614374L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { m_dice = DiceRoll.airBattle(m_attackingUnits, false, m_attacker, bridge, "Attackers Fire, "); } }; final IExecutable calculateCasualties = new IExecutable() { private static final long serialVersionUID = 4556409970663527142L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { m_details = BattleCalculator.selectCasualties(ATTACKERS_FIRE, m_defender, m_defendingUnits, m_defendingUnits, m_attacker, m_attackingUnits, false, new ArrayList<>(), m_battleSite, null, bridge, ATTACKERS_FIRE, m_dice, true, m_battleID, false, m_dice.getHits(), true); m_defendingWaitingToDie.addAll(m_details.getKilled()); markDamaged(m_details.getDamaged(), bridge); } }; final IExecutable notifyCasualties = new IExecutable() { private static final long serialVersionUID = 4224354422817922451L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { notifyCasualties(m_battleID, bridge, ATTACKERS_FIRE, m_dice, m_defender, m_attacker, m_details); } }; // push in reverse order of execution stack.push(notifyCasualties); stack.push(calculateCasualties); stack.push(roll); } } class DefendersFire implements IExecutable { private static final long serialVersionUID = -7277182945495744003L; DiceRoll m_dice; CasualtyDetails m_details; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_intercept) { return; } final IExecutable roll = new IExecutable() { private static final long serialVersionUID = 5953506121350176595L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingWaitingToDie); m_dice = DiceRoll.airBattle(m_defendingUnits, true, m_defender, bridge, "Defenders Fire, "); } }; final IExecutable calculateCasualties = new IExecutable() { private static final long serialVersionUID = 6658309931909306564L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { m_details = BattleCalculator.selectCasualties(DEFENDERS_FIRE, m_attacker, m_attackingUnits, m_attackingUnits, m_defender, m_defendingUnits, false, new ArrayList<>(), m_battleSite, null, bridge, DEFENDERS_FIRE, m_dice, false, m_battleID, false, m_dice.getHits(), true); m_attackingWaitingToDie.addAll(m_details.getKilled()); markDamaged(m_details.getDamaged(), bridge); } }; final IExecutable notifyCasualties = new IExecutable() { private static final long serialVersionUID = 4461950841000674515L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { notifyCasualties(m_battleID, bridge, DEFENDERS_FIRE, m_dice, m_attacker, m_defender, m_details); } }; // push in reverse order of execution stack.push(notifyCasualties); stack.push(calculateCasualties); stack.push(roll); } } private static Match<Unit> unitHasAirDefenseGreaterThanZero() { return new Match<Unit>() { @Override public boolean match(final Unit u) { return UnitAttachment.get(u.getType()).getAirDefense(u.getOwner()) > 0; } }; } private static Match<Unit> unitHasAirAttackGreaterThanZero() { return new Match<Unit>() { @Override public boolean match(final Unit u) { return UnitAttachment.get(u.getType()).getAirAttack(u.getOwner()) > 0; } }; } public static Match<Unit> attackingGroundSeaBattleEscorts(final PlayerID attacker, final GameData data) { return new Match<Unit>() { @Override public boolean match(final Unit u) { final CompositeMatch<Unit> canIntercept = new CompositeMatchAnd<>(Matches.unitCanAirBattle); return canIntercept.match(u); } }; } private static Match<Unit> defendingGroundSeaBattleInterceptors(final PlayerID attacker, final GameData data) { final boolean canScrambleIntoAirBattles = games.strategy.triplea.Properties.getCanScrambleIntoAirBattles(data); return new Match<Unit>() { @Override public boolean match(final Unit u) { final CompositeMatch<Unit> canIntercept = new CompositeMatchAnd<>(Matches.unitCanAirBattle, Matches.unitIsEnemyOf(data, attacker), Matches.UnitWasInAirBattle.invert()); if (!canScrambleIntoAirBattles) { canIntercept.add(Matches.UnitWasScrambled.invert()); } return canIntercept.match(u); } }; } private static Match<Unit> defendingBombingRaidInterceptors(final PlayerID attacker, final GameData data) { final boolean canScrambleIntoAirBattles = games.strategy.triplea.Properties.getCanScrambleIntoAirBattles(data); return new Match<Unit>() { @Override public boolean match(final Unit u) { final CompositeMatch<Unit> canIntercept = new CompositeMatchAnd<>(Matches.unitCanIntercept, Matches.unitIsEnemyOf(data, attacker), Matches.UnitWasInAirBattle.invert()); if (!canScrambleIntoAirBattles) { canIntercept.add(Matches.UnitWasScrambled.invert()); } return canIntercept.match(u); } }; } public static boolean territoryCouldPossiblyHaveAirBattleDefenders(final Territory territory, final PlayerID attacker, final GameData data, final boolean bombing) { final boolean canScrambleToAirBattle = games.strategy.triplea.Properties.getCanScrambleIntoAirBattles(data); final Match<Unit> defendingAirMatch = bombing ? defendingBombingRaidInterceptors(attacker, data) : defendingGroundSeaBattleInterceptors(attacker, data); int maxScrambleDistance = 0; if (canScrambleToAirBattle) { final Iterator<UnitType> utIter = data.getUnitTypeList().iterator(); while (utIter.hasNext()) { final UnitAttachment ua = UnitAttachment.get(utIter.next()); if (ua.getCanScramble() && maxScrambleDistance < ua.getMaxScrambleDistance()) { maxScrambleDistance = ua.getMaxScrambleDistance(); } } } else { return territory.getUnits().someMatch(defendingAirMatch); } // should we check if the territory also has an air base? return territory.getUnits().someMatch(defendingAirMatch) || Match.someMatch(data.getMap().getNeighbors(territory, maxScrambleDistance), Matches.territoryHasUnitsThatMatch(defendingAirMatch)); } public static int getAirBattleRolls(final Collection<Unit> units, final boolean defending) { int rolls = 0; for (final Unit u : units) { rolls += getAirBattleRolls(u, defending); } return rolls; } public static int getAirBattleRolls(final Unit unit, final boolean defending) { if (defending) { if (!unitHasAirDefenseGreaterThanZero().match(unit)) { return 0; } } else { if (!unitHasAirAttackGreaterThanZero().match(unit)) { return 0; } } return Math.max(0, (defending ? UnitAttachment.get(unit.getType()).getDefenseRolls(unit.getOwner()) : UnitAttachment.get(unit.getType()).getAttackRolls(unit.getOwner()))); } private void remove(final Collection<Unit> killed, final IDelegateBridge bridge, final Territory battleSite) { if (killed.size() == 0) { return; } final Collection<Unit> dependent = getDependentUnits(killed); killed.addAll(dependent); final Change killedChange = ChangeFactory.removeUnits(battleSite, killed); // m_killed.addAll(killed); final String transcriptText = MyFormatter.unitsToText(killed) + " lost in " + battleSite.getName(); bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(killed)); bridge.addChange(killedChange); final Collection<IBattle> dependentBattles = m_battleTracker.getBlocked(AirBattle.this); removeFromDependents(killed, bridge, dependentBattles, false); } private void notifyCasualties(final GUID battleID, final IDelegateBridge bridge, final String stepName, final DiceRoll dice, final PlayerID hitPlayer, final PlayerID firingPlayer, final CasualtyDetails details) { getDisplay(bridge).casualtyNotification(battleID, stepName, dice, hitPlayer, details.getKilled(), details.getDamaged(), Collections.emptyMap()); final Runnable r = () -> { try { getRemote(firingPlayer, bridge).confirmEnemyCasualties(battleID, "Press space to continue", hitPlayer); } catch (final Exception e) { ClientLogger.logQuietly(e); } }; // execute in a seperate thread to allow either player to click continue first. final Thread t = new Thread(r, "Click to continue waiter"); t.start(); getRemote(hitPlayer, bridge).confirmOwnCasualties(battleID, "Press space to continue"); try { bridge.leaveDelegateExecution(); t.join(); } catch (final InterruptedException e) { // ignore } finally { bridge.enterDelegateExecution(); } } private void removeFromDependents(final Collection<Unit> units, final IDelegateBridge bridge, final Collection<IBattle> dependents, final boolean withdrawn) { for (final IBattle dependent : dependents) { dependent.unitsLostInPrecedingBattle(this, units, bridge, withdrawn); } } @Override public boolean isEmpty() { return m_attackingUnits.isEmpty(); } @Override public void unitsLostInPrecedingBattle(final IBattle battle, final Collection<Unit> units, final IDelegateBridge bridge, final boolean withdrawn) { // should never happen // throw new IllegalStateException("AirBattle should not have any preceding battle with which to possibly remove // dependents from"); } }