package games.strategy.triplea.delegate; 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.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; 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.Territory; import games.strategy.engine.data.TerritoryEffect; 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.engine.message.ConnectionLostException; import games.strategy.sound.SoundPath; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.TechAbilityAttachment; import games.strategy.triplea.attachments.TechAttachment; 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.triplea.util.TransportUtils; import games.strategy.triplea.util.UnitSeperator; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.IntegerMap; import games.strategy.util.InverseMatch; import games.strategy.util.Match; import games.strategy.util.Util; /** * Handles logic for battles in which fighting actually occurs. */ public class MustFightBattle extends DependentBattle implements BattleStepStrings { public static enum ReturnFire { ALL, SUBS, NONE } public static enum RetreatType { DEFAULT, SUBS, PLANES, PARTIAL_AMPHIB } // this class exists for testing public abstract static class AttackSubs implements IExecutable { private static final long serialVersionUID = 4872551667582174716L; } // this class exists for testing public abstract static class DefendSubs implements IExecutable { private static final long serialVersionUID = 3768066729336520095L; } private static final long serialVersionUID = 5879502298361231540L; // maps Territory-> units (stores a collection of who is attacking from where, needed for undoing moves) private final Collection<Unit> m_attackingWaitingToDie = new ArrayList<>(); private final Collection<Unit> m_defendingWaitingToDie = new ArrayList<>(); // keep track of all the units that die in the battle to show in the history window private final Collection<Unit> m_killed = new ArrayList<>(); /** * Our current execution state, we keep a stack of executables, this allows us to save our state and resume while in * the middle of a * battle. */ private final ExecutionStack m_stack = new ExecutionStack(); private List<String> m_stepStrings; protected List<Unit> m_defendingAA; protected List<Unit> m_offensiveAA; protected List<String> m_defendingAAtypes; protected List<String> m_offensiveAAtypes; private final List<Unit> m_attackingUnitsRetreated = new ArrayList<>(); private final List<Unit> m_defendingUnitsRetreated = new ArrayList<>(); // -1 would mean forever until one side is eliminated (the default is infinite) private final int m_maxRounds; public MustFightBattle(final Territory battleSite, final PlayerID attacker, final GameData data, final BattleTracker battleTracker) { super(battleSite, attacker, battleTracker, false, BattleType.NORMAL, data); m_defendingUnits.addAll(m_battleSite.getUnits().getMatches(Matches.enemyUnit(attacker, data))); if (battleSite.isWater()) { m_maxRounds = games.strategy.triplea.Properties.getSeaBattleRounds(data); } else { m_maxRounds = games.strategy.triplea.Properties.getLandBattleRounds(data); } } public void resetDefendingUnits(final PlayerID attacker, final GameData data) { m_defendingUnits.clear(); m_defendingUnits.addAll(m_battleSite.getUnits().getMatches(Matches.enemyUnit(attacker, data))); } /** * Used for head-less battles. * * @param defending * - defending units * @param attacking * - attacking units * @param bombarding * - bombarding units * @param defender * - defender PlayerID */ public void setUnits(final Collection<Unit> defending, final Collection<Unit> attacking, final Collection<Unit> bombarding, final Collection<Unit> amphibious, final PlayerID defender, final Collection<TerritoryEffect> territoryEffects) { m_defendingUnits = new ArrayList<>(defending); m_attackingUnits = new ArrayList<>(attacking); m_bombardingUnits = new ArrayList<>(bombarding); m_amphibiousLandAttackers = new ArrayList<>(amphibious); m_isAmphibious = m_amphibiousLandAttackers.size() > 0; m_defender = defender; m_territoryEffects = territoryEffects; } public boolean shouldEndBattleDueToMaxRounds() { return m_maxRounds > 0 && m_maxRounds <= m_round; } private boolean canSubsSubmerge() { return games.strategy.triplea.Properties.getSubmersible_Subs(m_data); } @Override public void removeAttack(final Route route, final Collection<Unit> units) { m_attackingUnits.removeAll(units); // the route could be null, in the case of a unit in a territory where a sub is submerged. if (route == null) { return; } final Territory attackingFrom = route.getTerritoryBeforeEnd(); Collection<Unit> attackingFromMapUnits = m_attackingFromMap.get(attackingFrom); // handle possible null pointer if (attackingFromMapUnits == null) { attackingFromMapUnits = new ArrayList<>(); } attackingFromMapUnits.removeAll(units); if (attackingFromMapUnits.isEmpty()) { m_attackingFrom.remove(attackingFrom); } // deal with amphibious assaults if (attackingFrom.isWater()) { if (route.getEnd() != null && !route.getEnd().isWater() && Match.someMatch(units, Matches.UnitIsLand)) { m_amphibiousLandAttackers.removeAll(Match.getMatches(units, Matches.UnitIsLand)); } // if none of the units is a land unit, the attack from // that territory is no longer an amphibious assault if (Match.noneMatch(attackingFromMapUnits, Matches.UnitIsLand)) { m_amphibiousAttackFrom.remove(attackingFrom); // do we have any amphibious attacks left? m_isAmphibious = !m_amphibiousAttackFrom.isEmpty(); } } final Iterator<Unit> dependentHolders = m_dependentUnits.keySet().iterator(); while (dependentHolders.hasNext()) { final Unit holder = dependentHolders.next(); final Collection<Unit> dependents = m_dependentUnits.get(holder); dependents.removeAll(units); } } @Override public boolean isEmpty() { return m_attackingUnits.isEmpty() && m_attackingWaitingToDie.isEmpty(); } @Override public Change addAttackChange(final Route route, final Collection<Unit> units, final HashMap<Unit, HashSet<Unit>> targets) { final CompositeChange change = new CompositeChange(); // Filter out allied units if WW2V2 final Match<Unit> ownedBy = Matches.unitIsOwnedBy(m_attacker); final Collection<Unit> attackingUnits = isWW2V2() ? Match.getMatches(units, ownedBy) : units; final Territory attackingFrom = route.getTerritoryBeforeEnd(); m_attackingFrom.add(attackingFrom); m_attackingUnits.addAll(attackingUnits); if (m_attackingFromMap.get(attackingFrom) == null) { m_attackingFromMap.put(attackingFrom, new ArrayList<>()); } final Collection<Unit> attackingFromMapUnits = m_attackingFromMap.get(attackingFrom); attackingFromMapUnits.addAll(attackingUnits); // are we amphibious if (route.getStart().isWater() && route.getEnd() != null && !route.getEnd().isWater() && Match.someMatch(attackingUnits, Matches.UnitIsLand)) { m_amphibiousAttackFrom.add(route.getTerritoryBeforeEnd()); m_amphibiousLandAttackers.addAll(Match.getMatches(attackingUnits, Matches.UnitIsLand)); m_isAmphibious = true; } final Map<Unit, Collection<Unit>> dependencies = transporting(units); if (!isAlliedAirIndependent()) { dependencies.putAll(MoveValidator.carrierMustMoveWith(units, units, m_data, m_attacker)); for (final Unit carrier : dependencies.keySet()) { final UnitAttachment ua = UnitAttachment.get(carrier.getUnitType()); if (ua.getCarrierCapacity() == -1) { continue; } final Collection<Unit> fighters = dependencies.get(carrier); // Dependencies count both land and air units. Land units could be allied or owned, while air is just allied // since owned already // launched at beginning of turn fighters.retainAll(Match.getMatches(fighters, Matches.UnitIsAir)); for (final Unit fighter : fighters) { // Set transportedBy for fighter change.add(ChangeFactory.unitPropertyChange(fighter, carrier, TripleAUnit.TRANSPORTED_BY)); } // remove transported fighters from battle display m_attackingUnits.removeAll(fighters); } } // Set the dependent paratroopers so they die if the bomber dies. // TODO: this might be legacy code that can be deleted since we now keep paratrooper dependencies til they land (but // need to double // check) if (TechAttachment.isAirTransportable(m_attacker)) { final Collection<Unit> airTransports = Match.getMatches(units, Matches.UnitIsAirTransport); final Collection<Unit> paratroops = Match.getMatches(units, Matches.UnitIsAirTransportable); if (!airTransports.isEmpty() && !paratroops.isEmpty()) { // Load capable bombers by default> final Map<Unit, Unit> unitsToCapableAirTransports = TransportUtils.mapTransportsToLoad(paratroops, airTransports); final HashMap<Unit, Collection<Unit>> dependentUnits = new HashMap<>(); final Collection<Unit> singleCollection = new ArrayList<>(); for (final Unit unit : unitsToCapableAirTransports.keySet()) { final Collection<Unit> unitList = new ArrayList<>(); unitList.add(unit); final Unit bomber = unitsToCapableAirTransports.get(unit); singleCollection.add(unit); // Set transportedBy for paratrooper change.add(ChangeFactory.unitPropertyChange(unit, bomber, TripleAUnit.TRANSPORTED_BY)); // Set the dependents if (dependentUnits.get(bomber) != null) { dependentUnits.get(bomber).addAll(unitList); } else { dependentUnits.put(bomber, unitList); } } dependencies.putAll(dependentUnits); UnitSeperator.categorize(airTransports, dependentUnits, false, false); } } addDependentUnits(dependencies); // mark units with no movement // for all but air Collection<Unit> nonAir = Match.getMatches(attackingUnits, Matches.UnitIsNotAir); // we dont want to change the movement of transported land units if this is a sea battle // so restrict non air to remove land units if (m_battleSite.isWater()) { nonAir = Match.getMatches(nonAir, Matches.UnitIsNotLand); } // TODO This checks for ignored sub/trns and skips the set of the attackers to 0 movement left // If attacker stops in an occupied territory, movement stops (battle is optional) if (MoveValidator.onlyIgnoredUnitsOnPath(route, m_attacker, m_data, false)) { return change; } change.add(ChangeFactory.markNoMovementChange(nonAir)); return change; } public void addDependentUnits(final Map<Unit, Collection<Unit>> dependencies) { for (final Unit holder : dependencies.keySet()) { final Collection<Unit> transporting = dependencies.get(holder); if (m_dependentUnits.get(holder) != null) { m_dependentUnits.get(holder).addAll(transporting); } else { m_dependentUnits.put(holder, new LinkedHashSet<>(transporting)); } } } private String getBattleTitle() { return m_attacker.getName() + " attack " + m_defender.getName() + " in " + m_battleSite.getName(); } public void updateDefendingAAUnits() { final Collection<Unit> canFire = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); canFire.addAll(m_defendingUnits); canFire.addAll(m_defendingWaitingToDie); final HashMap<String, HashSet<UnitType>> airborneTechTargetsAllowed = TechAbilityAttachment.getAirborneTargettedByAA(m_attacker, m_data); m_defendingAA = Match.getMatches(canFire, Matches.UnitIsAAthatCanFire(m_attackingUnits, airborneTechTargetsAllowed, m_attacker, Matches.UnitIsAAforCombatOnly, m_round, true, m_data)); // comes ordered alphabetically m_defendingAAtypes = UnitAttachment.getAllOfTypeAAs(m_defendingAA); // stacks are backwards Collections.reverse(m_defendingAAtypes); } public void updateOffensiveAAUnits() { final Collection<Unit> canFire = new ArrayList<>(m_attackingUnits.size() + m_attackingWaitingToDie.size()); canFire.addAll(m_attackingUnits); canFire.addAll(m_attackingWaitingToDie); // no airborne targets for offensive aa m_offensiveAA = Match.getMatches(canFire, Matches.UnitIsAAthatCanFire(m_defendingUnits, new HashMap<>(), m_defender, Matches.UnitIsAAforCombatOnly, m_round, false, m_data)); // comes ordered alphabetically m_offensiveAAtypes = UnitAttachment.getAllOfTypeAAs(m_offensiveAA); // stacks are backwards Collections.reverse(m_offensiveAAtypes); } @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 have already started if (m_stack.isExecuting()) { final ITripleADisplay display = getDisplay(bridge); display.showBattle(m_battleID, m_battleSite, getBattleTitle(), removeNonCombatants(m_attackingUnits, true, false, false, false), removeNonCombatants(m_defendingUnits, false, false, false, false), m_killed, m_attackingWaitingToDie, m_defendingWaitingToDie, m_dependentUnits, m_attacker, m_defender, isAmphibious(), getBattleType(), m_amphibiousLandAttackers); display.listBattleSteps(m_battleID, m_stepStrings); m_stack.execute(bridge); return; } bridge.getHistoryWriter().startEvent("Battle in " + m_battleSite, m_battleSite); removeAirNoLongerInTerritory(); writeUnitsToHistory(bridge); // it is possible that no attacking units are present, if so end now // changed to only look at units that can be destroyed in combat, and therefore not include factories, aaguns, and // infrastructure. if (Match.getMatches(m_attackingUnits, Matches.UnitIsNotInfrastructure).size() == 0) { endBattle(bridge); defenderWins(bridge); return; } // it is possible that no defending units exist // changed to only look at units that can be destroyed in combat, and therefore not include factories, aaguns, and // infrastructure. if (Match.getMatches(m_defendingUnits, Matches.UnitIsNotInfrastructure).size() == 0) { endBattle(bridge); attackerWins(bridge); return; } addDependentUnits(transporting(m_defendingUnits)); addDependentUnits(transporting(m_attackingUnits)); // determine any AA updateOffensiveAAUnits(); updateDefendingAAUnits(); // list the steps m_stepStrings = determineStepStrings(true, bridge); final ITripleADisplay display = getDisplay(bridge); display.showBattle(m_battleID, m_battleSite, getBattleTitle(), removeNonCombatants(m_attackingUnits, true, false, false, false), removeNonCombatants(m_defendingUnits, false, false, false, false), m_killed, m_attackingWaitingToDie, m_defendingWaitingToDie, m_dependentUnits, m_attacker, m_defender, isAmphibious(), getBattleType(), m_amphibiousLandAttackers); display.listBattleSteps(m_battleID, m_stepStrings); if (!m_headless) { // take the casualties with least movement first if (isAmphibious()) { sortAmphib(m_attackingUnits, m_data); } else { BattleCalculator.sortPreBattle(m_attackingUnits); } BattleCalculator.sortPreBattle(m_defendingUnits); // play a sound if (Match.someMatch(m_attackingUnits, Matches.UnitIsSea) || Match.someMatch(m_defendingUnits, Matches.UnitIsSea)) { if (Match.allMatch(m_attackingUnits, Matches.UnitIsSub) || (Match.someMatch(m_attackingUnits, Matches.UnitIsSub) && Match.someMatch(m_defendingUnits, Matches.UnitIsSub))) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_SEA_SUBS, m_attacker); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_SEA_NORMAL, m_attacker); } } else if (Match.allMatch(m_attackingUnits, Matches.UnitIsAir) && Match.allMatch(m_defendingUnits, Matches.UnitIsAir)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR, m_attacker); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_LAND, m_attacker); } } // push on stack in opposite order of execution pushFightLoopOnStack(true); m_stack.execute(bridge); } private void writeUnitsToHistory(final IDelegateBridge bridge) { if (m_headless) { return; } final Set<PlayerID> playerSet = m_battleSite.getUnits().getPlayersWithUnits(); String transcriptText; // find all attacking players (unsorted) final Collection<PlayerID> attackers = new ArrayList<>(); final Collection<Unit> allAttackingUnits = new ArrayList<>(); transcriptText = ""; for (final PlayerID current : playerSet) { if (m_data.getRelationshipTracker().isAllied(m_attacker, current) || current.equals(m_attacker)) { attackers.add(current); } } // find all attacking units (unsorted) for (final Iterator<PlayerID> attackersIter = attackers.iterator(); attackersIter.hasNext();) { final PlayerID current = attackersIter.next(); String delim; if (attackersIter.hasNext()) { delim = "; "; } else { delim = ""; } final Collection<Unit> attackingUnits = Match.getMatches(m_attackingUnits, Matches.unitIsOwnedBy(current)); final String verb = current.equals(m_attacker) ? "attack" : "loiter and taunt"; transcriptText += current.getName() + " " + verb + (attackingUnits.isEmpty() ? "" : " with " + MyFormatter.unitsToTextNoOwner(attackingUnits)) + delim; allAttackingUnits.addAll(attackingUnits); // If any attacking transports are in the battle, set their status to later restrict load/unload if (current.equals(m_attacker)) { final CompositeChange change = new CompositeChange(); final Collection<Unit> transports = Match.getMatches(attackingUnits, Matches.UnitCanTransport); final Iterator<Unit> attackTranIter = transports.iterator(); while (attackTranIter.hasNext()) { change.add(ChangeFactory.unitPropertyChange(attackTranIter.next(), true, TripleAUnit.WAS_IN_COMBAT)); } bridge.addChange(change); } } // write attacking units to history if (m_attackingUnits.size() > 0) { bridge.getHistoryWriter().addChildToEvent(transcriptText, allAttackingUnits); } // find all defending players (unsorted) final Collection<PlayerID> defenders = new ArrayList<>(); final Collection<Unit> allDefendingUnits = new ArrayList<>(); transcriptText = ""; for (final PlayerID current : playerSet) { if (m_data.getRelationshipTracker().isAllied(m_defender, current) || current.equals(m_defender)) { defenders.add(current); } } // find all defending units (unsorted) for (final Iterator<PlayerID> defendersIter = defenders.iterator(); defendersIter.hasNext();) { final PlayerID current = defendersIter.next(); Collection<Unit> defendingUnits; String delim; if (defendersIter.hasNext()) { delim = "; "; } else { delim = ""; } defendingUnits = Match.getMatches(m_defendingUnits, Matches.unitIsOwnedBy(current)); transcriptText += current.getName() + " defend with " + MyFormatter.unitsToTextNoOwner(defendingUnits) + delim; allDefendingUnits.addAll(defendingUnits); } // write defending units to history if (m_defendingUnits.size() > 0) { bridge.getHistoryWriter().addChildToEvent(transcriptText, allDefendingUnits); } } private void removeAirNoLongerInTerritory() { if (m_headless) { return; } // remove any air units that were once in this attack, but have now // moved out of the territory // this is an ilegant way to handle this bug final CompositeMatch<Unit> airNotInTerritory = new CompositeMatchAnd<>(); airNotInTerritory.add(new InverseMatch<>(Matches.unitIsInTerritory(m_battleSite))); m_attackingUnits.removeAll(Match.getMatches(m_attackingUnits, airNotInTerritory)); } public List<String> determineStepStrings(final boolean showFirstRun, final IDelegateBridge bridge) { final List<String> steps = new ArrayList<>(); if (canFireOffensiveAA()) { for (final String typeAA : UnitAttachment.getAllOfTypeAAs(m_offensiveAA)) { steps.add(m_attacker.getName() + " " + typeAA + AA_GUNS_FIRE_SUFFIX); steps.add(m_defender.getName() + SELECT_PREFIX + typeAA + CASUALTIES_SUFFIX); steps.add(m_defender.getName() + REMOVE_PREFIX + typeAA + CASUALTIES_SUFFIX); } } if (canFireDefendingAA()) { for (final String typeAA : UnitAttachment.getAllOfTypeAAs(m_defendingAA)) { steps.add(m_defender.getName() + " " + typeAA + AA_GUNS_FIRE_SUFFIX); steps.add(m_attacker.getName() + SELECT_PREFIX + typeAA + CASUALTIES_SUFFIX); steps.add(m_attacker.getName() + REMOVE_PREFIX + typeAA + CASUALTIES_SUFFIX); } } if (showFirstRun) { if (!m_battleSite.isWater() && !getBombardingUnits().isEmpty()) { steps.add(NAVAL_BOMBARDMENT); steps.add(SELECT_NAVAL_BOMBARDMENT_CASUALTIES); } if (Match.someMatch(m_attackingUnits, Matches.UnitIsSuicide)) { steps.add(SUICIDE_ATTACK); steps.add(m_defender.getName() + SELECT_CASUALTIES_SUICIDE); } if (Match.someMatch(m_defendingUnits, Matches.UnitIsSuicide) && !isDefendingSuicideAndMunitionUnitsDoNotFire()) { steps.add(SUICIDE_DEFEND); steps.add(m_attacker.getName() + SELECT_CASUALTIES_SUICIDE); } if (!m_battleSite.isWater() && TechAttachment.isAirTransportable(m_attacker)) { final Collection<Unit> bombers = Match.getMatches(m_battleSite.getUnits().getUnits(), Matches.UnitIsAirTransport); if (!bombers.isEmpty()) { final Collection<Unit> dependents = getDependentUnits(bombers); if (!dependents.isEmpty()) { steps.add(LAND_PARATROOPS); } } } } // Check if defending subs can submerge before battle if (isSubRetreatBeforeBattle()) { if (!Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer) && Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { steps.add(m_attacker.getName() + SUBS_SUBMERGE); } if (!Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer) && Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { steps.add(m_defender.getName() + SUBS_SUBMERGE); } } // See if there any unescorted trns if (m_battleSite.isWater() && isTransportCasualtiesRestricted()) { if (Match.someMatch(m_attackingUnits, Matches.UnitIsTransport) || Match.someMatch(m_defendingUnits, Matches.UnitIsTransport)) { steps.add(REMOVE_UNESCORTED_TRANSPORTS); } } // if attacker has no sneak attack subs, then defendering sneak attack subs fire first and remove casualties final boolean defenderSubsFireFirst = defenderSubsFireFirst(); if (defenderSubsFireFirst && Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { steps.add(m_defender.getName() + SUBS_FIRE); steps.add(m_attacker.getName() + SELECT_SUB_CASUALTIES); steps.add(REMOVE_SNEAK_ATTACK_CASUALTIES); } final boolean onlyAttackerSneakAttack = !defenderSubsFireFirst && returnFireAgainstAttackingSubs() == ReturnFire.NONE && returnFireAgainstDefendingSubs() == ReturnFire.ALL; // attacker subs sneak attack // Attacking subs have no sneak attack if Destroyers are present if (m_battleSite.isWater()) { if (Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { steps.add(m_attacker.getName() + SUBS_FIRE); steps.add(m_defender.getName() + SELECT_SUB_CASUALTIES); } if (onlyAttackerSneakAttack) { steps.add(REMOVE_SNEAK_ATTACK_CASUALTIES); } } // ww2v2 rules, all subs fire FIRST in combat, regardless of presence of destroyers. final boolean defendingSubsFireWithAllDefenders = !defenderSubsFireFirst && !games.strategy.triplea.Properties.getWW2V2(m_data) && returnFireAgainstDefendingSubs() == ReturnFire.ALL; // defender subs sneak attack // Defending subs have no sneak attack in Pacific/Europe Theaters or if Destroyers are present final boolean defendingSubsFireWithAllDefendersAlways = !defendingSubsSneakAttack3(); if (m_battleSite.isWater()) { if (!defendingSubsFireWithAllDefendersAlways && !defendingSubsFireWithAllDefenders && !defenderSubsFireFirst && Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { steps.add(m_defender.getName() + SUBS_FIRE); steps.add(m_attacker.getName() + SELECT_SUB_CASUALTIES); } } if (m_battleSite.isWater() && !defenderSubsFireFirst && !onlyAttackerSneakAttack && (returnFireAgainstDefendingSubs() != ReturnFire.ALL || returnFireAgainstAttackingSubs() != ReturnFire.ALL)) { steps.add(REMOVE_SNEAK_ATTACK_CASUALTIES); } // Air only Units can't attack subs without Destroyers present if (isAirAttackSubRestricted()) { final Collection<Unit> units = new ArrayList<>(m_attackingUnits.size() + m_attackingWaitingToDie.size()); units.addAll(m_attackingUnits); // if(!Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer) && Match.allMatch(m_attackingUnits, // Matches.UnitIsAir)) if (Match.someMatch(m_attackingUnits, Matches.UnitIsAir) && !canAirAttackSubs(m_defendingUnits, units)) { steps.add(SUBMERGE_SUBS_VS_AIR_ONLY); } } // Air Units can't attack subs without Destroyers present if (m_battleSite.isWater() && isAirAttackSubRestricted()) { final Collection<Unit> units = new ArrayList<>(m_attackingUnits.size() + m_attackingWaitingToDie.size()); units.addAll(m_attackingUnits); // if(!Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer) && Match.someMatch(m_attackingUnits, // Matches.UnitIsAir) && // Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) if (Match.someMatch(m_attackingUnits, Matches.UnitIsAir) && !canAirAttackSubs(m_defendingUnits, units)) { steps.add(AIR_ATTACK_NON_SUBS); } } if (Match.someMatch(m_attackingUnits, Matches.UnitIsNotSub)) { steps.add(m_attacker.getName() + FIRE); steps.add(m_defender.getName() + SELECT_CASUALTIES); } // classic rules, subs fire with all defenders // also, ww2v3/global rules, defending subs without sneak attack fire with all defenders if (m_battleSite.isWater()) { final Collection<Unit> units = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); units.addAll(m_defendingUnits); units.addAll(m_defendingWaitingToDie); if (Match.someMatch(units, Matches.UnitIsSub) && !defenderSubsFireFirst && (defendingSubsFireWithAllDefenders || defendingSubsFireWithAllDefendersAlways)) { steps.add(m_defender.getName() + SUBS_FIRE); steps.add(m_attacker.getName() + SELECT_SUB_CASUALTIES); } } // Air Units can't attack subs without Destroyers present if (m_battleSite.isWater() && isAirAttackSubRestricted()) { final Collection<Unit> units = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); units.addAll(m_defendingUnits); units.addAll(m_defendingWaitingToDie); // if(!Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer) && Match.someMatch(m_defendingUnits, // Matches.UnitIsAir) && // Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) if (Match.someMatch(m_defendingUnits, Matches.UnitIsAir) && !canAirAttackSubs(m_attackingUnits, units)) { steps.add(AIR_DEFEND_NON_SUBS); } } if (Match.someMatch(m_defendingUnits, Matches.UnitIsNotSub)) { steps.add(m_defender.getName() + FIRE); steps.add(m_attacker.getName() + SELECT_CASUALTIES); } // remove casualties steps.add(REMOVE_CASUALTIES); // retreat subs if (m_battleSite.isWater()) { if (canSubsSubmerge()) { if (!isSubRetreatBeforeBattle()) { if (Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { steps.add(m_attacker.getName() + SUBS_SUBMERGE); } if (Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { steps.add(m_defender.getName() + SUBS_SUBMERGE); } } } else { if (canAttackerRetreatSubs()) { if (Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { steps.add(m_attacker.getName() + SUBS_WITHDRAW); } } if (canDefenderRetreatSubs()) { if (Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { steps.add(m_defender.getName() + SUBS_WITHDRAW); } } } } // if we are a sea zone, then we may not be able to retreat // (ie a sub travelled under another unit to get to the battle site) // or an enemy sub retreated to our sea zone // however, if all our sea units die, then // the air units can still retreat, so if we have any air units attacking in // a sea zone, we always have to have the retreat // option shown // later, if our sea units die, we may ask the user to retreat final boolean someAirAtSea = m_battleSite.isWater() && Match.someMatch(m_attackingUnits, Matches.UnitIsAir); if (canAttackerRetreat() || someAirAtSea) { steps.add(m_attacker.getName() + ATTACKER_WITHDRAW); } else if (canAttackerRetreatPartialAmphib()) { steps.add(m_attacker.getName() + NONAMPHIB_WITHDRAW); } else if (canAttackerRetreatPlanes()) { steps.add(m_attacker.getName() + PLANES_WITHDRAW); } return steps; } private boolean defenderSubsFireFirst() { return returnFireAgainstAttackingSubs() == ReturnFire.ALL && returnFireAgainstDefendingSubs() == ReturnFire.NONE; } private void addFightStartToStack(final boolean firstRun, final List<IExecutable> steps) { final boolean offensiveAA = canFireOffensiveAA(); final boolean defendingAA = canFireDefendingAA(); if (offensiveAA) { steps.add(new IExecutable() { private static final long serialVersionUID = 3802352588499530533L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireOffensiveAAGuns(); } }); } if (defendingAA) { steps.add(new IExecutable() { private static final long serialVersionUID = -1370090785540214199L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireDefensiveAAGuns(); } }); } if (offensiveAA || defendingAA) { steps.add(new IExecutable() { private static final long serialVersionUID = 8762796262264296436L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { clearWaitingToDie(bridge); } }); } if (m_round > 1) { steps.add(new IExecutable() { private static final long serialVersionUID = 2781652892457063082L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { removeNonCombatants(bridge, false, false, true); } }); } if (firstRun) { steps.add(new IExecutable() { private static final long serialVersionUID = -2255284529092427441L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireNavalBombardment(bridge); } }); steps.add(new IExecutable() { private static final long serialVersionUID = 6578267830066963474L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireSuicideUnitsAttack(); } }); steps.add(new IExecutable() { private static final long serialVersionUID = 2731652892447063082L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { fireSuicideUnitsDefend(); } }); steps.add(new IExecutable() { private static final long serialVersionUID = 3389635558184415797L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { removeNonCombatants(bridge, false, false, true); } }); steps.add(new IExecutable() { private static final long serialVersionUID = 7193353768857658286L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { landParatroops(bridge); } }); steps.add(new IExecutable() { private static final long serialVersionUID = -6676316363537467594L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { markNoMovementLeft(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; } List<IExecutable> getBattleExecutables(final boolean firstRun) { // the code here is a bit odd to read // basically, we need to break the code into seperate atomic pieces. // If there is a network error, or some other unfortunate event, // then we need to keep track of what pieces we have executed, and what is left // to do // each atomic step is in its own IExecutable // the definition of atomic is that either // 1) the code does not call to an IDisplay,IPlayer, or IRandomSource // 2) if the code calls to an IDisplay, IPlayer, IRandomSource, and an exception is // called from one of those methods, the exception will be propogated out of execute(), // and the execute method can be called again // it is allowed for an iexecutable to add other iexecutables to the stack // if you read the code in linear order, ignore wrapping stuff in annonymous iexecutables, then the code // can be read as it will execute // store the steps in a list // we need to push them in reverse order that we // create them, and its easier to track if we just add them // to a list while creating. then reverse the list and add // to the stack at the end final List<IExecutable> steps = new ArrayList<>(); addFightStartToStack(firstRun, steps); addFightStepsNonEditMode(steps); /* * FYI: according to the rules that I know, you can submerge subs the same turn you kill the last destroyer * // we must grab these here, when we clear waiting to die, we might remove * // all the opposing destroyers, and this would change the canRetreatSubs rVal * final boolean canAttackerRetreatSubs = canAttackerRetreatSubs(); * final boolean canDefenderRetreatSubs = canDefenderRetreatSubs(); */ steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 8611067962952500496L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { Match<Unit> unitList = Matches.UnitCanBeInBattle(true, !m_battleSite.isWater(), 1, false, true, true); final List<Unit> sortedAttackingUnits = new ArrayList<>(Match.getMatches(m_attackingUnits, unitList)); Collections.sort(sortedAttackingUnits, new UnitBattleComparator(false, BattleCalculator.getCostsForTUV(bridge.getPlayerID(), m_data), TerritoryEffectHelper.getEffects(m_battleSite), m_data, false, false)); Collections.reverse(sortedAttackingUnits); if (DiceRoll.getTotalPower( DiceRoll.getUnitPowerAndRollsForNormalBattles(sortedAttackingUnits, m_defendingUnits, false, false, m_data, m_battleSite, TerritoryEffectHelper.getEffects(m_battleSite), false, null), m_data) > 0) { final List<Unit> sortedDefendingUnits = new ArrayList<>(Match.getMatches(m_defendingUnits, unitList)); Collections.sort(sortedDefendingUnits, new UnitBattleComparator(false, BattleCalculator.getCostsForTUV(bridge.getPlayerID(), m_data), TerritoryEffectHelper.getEffects(m_battleSite), m_data, false, false)); Collections.reverse(sortedDefendingUnits); if (DiceRoll.getTotalPower( DiceRoll.getUnitPowerAndRollsForNormalBattles(sortedDefendingUnits, m_defendingUnits, false, false, m_data, m_battleSite, TerritoryEffectHelper.getEffects(m_battleSite), false, null), m_data) == 0) { remove(m_defendingUnits, bridge, m_battleSite, true); } } clearWaitingToDie(bridge); } }); steps.add(new IExecutable() { // not compatible with 0.9.0.2 saved games. this is new for 1.2.6.0 private static final long serialVersionUID = 6387198382888361848L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { checkSuicideUnits(bridge); } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 5259103822937067667L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { // changed to only look at units that can be destroyed in combat, and therefore not include factories, aaguns, // and infrastructure. if (Match.getMatches(m_attackingUnits, Matches.UnitIsNotInfrastructure).size() == 0) { if (!isTransportCasualtiesRestricted()) { endBattle(bridge); defenderWins(bridge); } else { // Get all allied transports in the territory final CompositeMatch<Unit> matchAllied = new CompositeMatchAnd<>(); matchAllied.add(Matches.UnitIsTransport); matchAllied.add(Matches.UnitIsNotCombatTransport); matchAllied.add(Matches.isUnitAllied(m_attacker, m_data)); final List<Unit> alliedTransports = Match.getMatches(m_battleSite.getUnits().getUnits(), matchAllied); // If no transports, just end the battle if (alliedTransports.isEmpty()) { endBattle(bridge); defenderWins(bridge); } else if (m_round <= 1) { // TODO Need to determine how combined forces on attack work- trn left in terr by prev player, ally moves // in and attacks // add back in the non-combat units (Trns) m_attackingUnits = Match.getMatches(m_battleSite.getUnits().getUnits(), Matches.unitIsOwnedBy(m_attacker)); } else { endBattle(bridge); defenderWins(bridge); } } // changed to only look at units that can be destroyed in combat, and therefore not include factories, aaguns, // and infrastructure. } else if (Match.getMatches(m_defendingUnits, Matches.UnitIsNotInfrastructure).size() == 0) { if (isTransportCasualtiesRestricted()) { // If there are undefended attacking transports, determine if they automatically die checkUndefendedTransports(bridge, m_defender); } checkForUnitsThatCanRollLeft(bridge, false); endBattle(bridge); attackerWins(bridge); } else if (shouldEndBattleDueToMaxRounds() || (Match.allMatch(m_attackingUnits, Matches.unitHasAttackValueOfAtLeast(1).invert()) && Match.allMatch(m_defendingUnits, Matches.unitHasDefendValueOfAtLeast(1).invert()))) { endBattle(bridge); nobodyWins(bridge); } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 6775880082912594489L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver && canAttackerRetreatSubs() && !isSubRetreatBeforeBattle()) { attackerRetreatSubs(bridge); } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -1544916305666912480L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { if (canDefenderRetreatSubs() && !isSubRetreatBeforeBattle()) { defenderRetreatSubs(bridge); } // Here we test if there are any defenders left. If no defenders, then battle is over. // The reason we test a "second" time here, is because otherwise the attackers can retreat even though the // battle is over // (illegal). if (m_defendingUnits.isEmpty()) { endBattle(bridge); attackerWins(bridge); } } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -1150863964807721395L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver && canAttackerRetreatPlanes() && !canAttackerRetreatPartialAmphib()) { attackerRetreatPlanes(bridge); } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -1150863964807721395L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver && canAttackerRetreatPartialAmphib()) { attackerRetreatNonAmphibUnits(bridge); } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 669349383898975048L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { attackerRetreat(bridge); } } }); final IExecutable loop = new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 3118458517320468680L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { pushFightLoopOnStack(false); } }; steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -3993599528368570254L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { m_round++; // determine any AA updateOffensiveAAUnits(); updateDefendingAAUnits(); m_stepStrings = determineStepStrings(false, bridge); final ITripleADisplay display = getDisplay(bridge); display.listBattleSteps(m_battleID, m_stepStrings); // 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; } private void addFightStepsNonEditMode(final List<IExecutable> steps) { /** Ask to retreat defending subs before battle */ if (isSubRetreatBeforeBattle()) { steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 6775880082912594489L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { attackerRetreatSubs(bridge); } } }); steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 7056448091800764539L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!m_isOver) { defenderRetreatSubs(bridge); } } }); } /** Remove Suicide Units */ steps.add(new IExecutable() { private static final long serialVersionUID = 99988L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { checkSuicideUnits(bridge); } }); /** Remove undefended trns */ if (isTransportCasualtiesRestricted()) { steps.add(new IExecutable() { private static final long serialVersionUID = 99989L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { checkUndefendedTransports(bridge, m_defender); checkUndefendedTransports(bridge, m_attacker); checkForUnitsThatCanRollLeft(bridge, true); checkForUnitsThatCanRollLeft(bridge, false); } }); } /** Submerge subs if -vs air only & air restricted from attacking subs */ if (isAirAttackSubRestricted()) { steps.add(new IExecutable() { private static final long serialVersionUID = 99990L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { submergeSubsVsOnlyAir(bridge); } }); } final ReturnFire returnFireAgainstAttackingSubs = returnFireAgainstAttackingSubs(); final ReturnFire returnFireAgainstDefendingSubs = returnFireAgainstDefendingSubs(); if (defenderSubsFireFirst()) { steps.add(new DefendSubs() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 99992L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { defendSubs(returnFireAgainstDefendingSubs); } }); } steps.add(new AttackSubs() { private static final long serialVersionUID = 99991L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { attackSubs(returnFireAgainstAttackingSubs); } }); final boolean defendingSubsFireWithAllDefenders = !defenderSubsFireFirst() && !games.strategy.triplea.Properties.getWW2V2(m_data) && returnFireAgainstDefendingSubs() == ReturnFire.ALL; if (defendingSubsSneakAttack3() && !defenderSubsFireFirst() && !defendingSubsFireWithAllDefenders) { steps.add(new DefendSubs() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 99992L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { defendSubs(returnFireAgainstDefendingSubs); } }); } /** Attacker air fire on NON subs */ if (isAirAttackSubRestricted()) { steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 99993L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { attackAirOnNonSubs(); } }); } /** Attacker fire remaining units */ steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 99994L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { attackNonSubs(); } }); if (!defenderSubsFireFirst() && (!defendingSubsSneakAttack3() || defendingSubsFireWithAllDefenders)) { steps.add(new DefendSubs() { private static final long serialVersionUID = 999921L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { defendSubs(returnFireAgainstDefendingSubs); } }); } /** Defender air fire on NON subs */ if (isAirAttackSubRestricted()) { steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 1560702114917865123L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { defendAirOnNonSubs(); } }); } steps.add(new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = 1560702114917865290L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { defendNonSubs(); } }); } private ReturnFire returnFireAgainstAttackingSubs() { final boolean attackingSubsSneakAttack = !Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer); final boolean defendingSubsSneakAttack = defendingSubsSneakAttack2(); final ReturnFire returnFireAgainstAttackingSubs; if (!attackingSubsSneakAttack) { returnFireAgainstAttackingSubs = ReturnFire.ALL; } else if (defendingSubsSneakAttack || isWW2V2()) { returnFireAgainstAttackingSubs = ReturnFire.SUBS; } else { returnFireAgainstAttackingSubs = ReturnFire.NONE; } return returnFireAgainstAttackingSubs; } private ReturnFire returnFireAgainstDefendingSubs() { /* Attacker subs fire * * calculate here, this holds for the fight round, but can't be computed later * since destroyers may die */ final boolean attackingSubsSneakAttack = !Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer); final boolean defendingSubsSneakAttack = defendingSubsSneakAttack2(); final ReturnFire returnFireAgainstDefendingSubs; if (!defendingSubsSneakAttack) { returnFireAgainstDefendingSubs = ReturnFire.ALL; } else if (attackingSubsSneakAttack || isWW2V2()) { returnFireAgainstDefendingSubs = ReturnFire.SUBS; } else { returnFireAgainstDefendingSubs = ReturnFire.NONE; } return returnFireAgainstDefendingSubs; } private boolean defendingSubsSneakAttack2() { return !Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer) && defendingSubsSneakAttack3(); } private boolean defendingSubsSneakAttack3() { return isWW2V2() || isDefendingSubsSneakAttack(); } private boolean canAttackerRetreatPlanes() { return (isWW2V2() || isAttackerRetreatPlanes() || isPartialAmphibiousRetreat()) && m_isAmphibious && Match.someMatch(m_attackingUnits, Matches.UnitIsAir); } private boolean canAttackerRetreatPartialAmphib() { if (m_isAmphibious && isPartialAmphibiousRetreat()) { // Only include land units when checking for allow amphibious retreat final List<Unit> landUnits = Match.getMatches(m_attackingUnits, Matches.UnitIsLand); for (final Unit unit : landUnits) { final TripleAUnit taUnit = (TripleAUnit) unit; if (!taUnit.getWasAmphibious()) { return true; } } } return false; } Collection<Territory> getAttackerRetreatTerritories() { // TODO: when attacking with paratroopers (air + carried land), there are several bugs in retreating. // TODO: air should always be able to retreat. paratrooped land units can only retreat if there are other // non-paratrooper non-amphibious // land units. // If attacker is all planes, just return collection of current territory if (m_headless || Match.allMatch(m_attackingUnits, Matches.UnitIsAir) || games.strategy.triplea.Properties.getRetreatingUnitsRemainInPlace(m_data)) { final Collection<Territory> oneTerritory = new ArrayList<>(2); oneTerritory.add(m_battleSite); return oneTerritory; } // its possible that a sub retreated to a territory we came from, if so we can no longer retreat there // or if we are moving out of a territory containing enemy units, we cannot retreat back there final CompositeMatchAnd<Unit> enemyUnitsThatPreventRetreat = new CompositeMatchAnd<>(Matches.enemyUnit(m_attacker, m_data), Matches.UnitIsNotInfrastructure, Matches.unitIsBeingTransported().invert(), Matches.UnitIsSubmerged.invert()); if (games.strategy.triplea.Properties.getIgnoreSubInMovement(m_data)) { enemyUnitsThatPreventRetreat.add(Matches.UnitIsNotSub); } if (games.strategy.triplea.Properties.getIgnoreTransportInMovement(m_data)) { enemyUnitsThatPreventRetreat.add(Matches.UnitIsNotTransportButCouldBeCombatTransport); } Collection<Territory> possible = Match.getMatches(m_attackingFrom, Matches.territoryHasUnitsThatMatch(enemyUnitsThatPreventRetreat).invert()); // In WW2V2 and WW2V3 we need to filter out territories where only planes // came from since planes cannot define retreat paths if (isWW2V2() || isWW2V3()) { possible = Match.getMatches(possible, new Match<Territory>() { @Override public boolean match(final Territory t) { final Collection<Unit> units = m_attackingFromMap.get(t); return !Match.allMatch(units, Matches.UnitIsAir); } }); } // the air unit may have come from a conquered or enemy territory, don't allow retreating final Match<Territory> conqueuredOrEnemy = new CompositeMatchOr<>( Matches.isTerritoryEnemyAndNotUnownedWaterOrImpassableOrRestricted(m_attacker, m_data), new CompositeMatchAnd<Territory>( // Matches.TerritoryIsLand, Matches.TerritoryIsWater, Matches.territoryWasFoughOver(m_battleTracker))); possible.removeAll(Match.getMatches(possible, conqueuredOrEnemy)); // the battle site is in the attacking from // if sea units are fighting a submerged sub possible.remove(m_battleSite); if (Match.someMatch(m_attackingUnits, Matches.UnitIsLand) && !m_battleSite.isWater()) { possible = Match.getMatches(possible, Matches.TerritoryIsLand); } if (Match.someMatch(m_attackingUnits, Matches.UnitIsSea)) { possible = Match.getMatches(possible, Matches.TerritoryIsWater); } return possible; } private boolean canAttackerRetreat() { if (onlyDefenselessDefendingTransportsLeft()) { return false; } if (m_isAmphibious) { return false; } final Collection<Territory> options = getAttackerRetreatTerritories(); return options.size() != 0; } private boolean onlyDefenselessDefendingTransportsLeft() { if (!isTransportCasualtiesRestricted()) { return false; } return Match.allMatch(m_defendingUnits, Matches.UnitIsTransportButNotCombatTransport); } private boolean canAttackerRetreatSubs() { if (Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer)) { return false; } if (Match.someMatch(m_defendingWaitingToDie, Matches.UnitIsDestroyer)) { return false; } return canAttackerRetreat() || canSubsSubmerge(); } // Added for test case calls void externalRetreat(final Collection<Unit> retreaters, final Territory retreatTo, final boolean defender, final IDelegateBridge bridge) { m_isOver = true; retreatUnits(retreaters, retreatTo, defender, bridge); } private void attackerRetreat(final IDelegateBridge bridge) { if (!canAttackerRetreat()) { return; } final Collection<Territory> possible = getAttackerRetreatTerritories(); if (!m_isOver) { if (m_isAmphibious) { queryRetreat(false, RetreatType.PARTIAL_AMPHIB, bridge, possible); } else { queryRetreat(false, RetreatType.DEFAULT, bridge, possible); } } } private void attackerRetreatPlanes(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 (Match.someMatch(m_attackingUnits, Matches.UnitIsAir)) { queryRetreat(false, RetreatType.PLANES, bridge, possible); } } private void attackerRetreatNonAmphibUnits(final IDelegateBridge bridge) { final Collection<Territory> possible = getAttackerRetreatTerritories(); queryRetreat(false, RetreatType.PARTIAL_AMPHIB, bridge, possible); } private boolean canDefenderRetreatSubs() { if (Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer)) { return false; } if (Match.someMatch(m_attackingWaitingToDie, Matches.UnitIsDestroyer)) { return false; } return getEmptyOrFriendlySeaNeighbors(m_defender, Match.getMatches(m_defendingUnits, Matches.UnitIsSub)).size() != 0 || canSubsSubmerge(); } private void attackerRetreatSubs(final IDelegateBridge bridge) { if (!canAttackerRetreatSubs()) { return; } if (Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { queryRetreat(false, RetreatType.SUBS, bridge, getAttackerRetreatTerritories()); } } private void defenderRetreatSubs(final IDelegateBridge bridge) { if (!canDefenderRetreatSubs()) { return; } if (!m_isOver && Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { queryRetreat(true, RetreatType.SUBS, bridge, getEmptyOrFriendlySeaNeighbors(m_defender, Match.getMatches(m_defendingUnits, Matches.UnitIsSub))); } } private Collection<Territory> getEmptyOrFriendlySeaNeighbors(final PlayerID player, final Collection<Unit> unitsToRetreat) { Collection<Territory> possible = m_data.getMap().getNeighbors(m_battleSite); if (m_headless) { return possible; } final CompositeMatch<Territory> match = new CompositeMatchAnd<>(Matches.TerritoryIsWater, Matches.territoryHasNoEnemyUnits(player, m_data)); // make sure we can move through the any canals final Match<Territory> canalMatch = new Match<Territory>() { @Override public boolean match(final Territory t) { final Route r = new Route(); r.setStart(m_battleSite); r.add(t); return MoveValidator.validateCanal(r, unitsToRetreat, m_defender, m_data) == null; } }; match.add(canalMatch); possible = Match.getMatches(possible, match); return possible; } private void queryRetreat(final boolean defender, final RetreatType retreatType, final IDelegateBridge bridge, Collection<Territory> availableTerritories) { boolean subs; boolean planes; boolean partialAmphib; planes = retreatType == RetreatType.PLANES; subs = retreatType == RetreatType.SUBS; final boolean canSubsSubmerge = canSubsSubmerge(); final boolean submerge = subs && canSubsSubmerge; final boolean canDefendingSubsSubmergeOrRetreat = subs && defender && games.strategy.triplea.Properties.getSubmarinesDefendingMaySubmergeOrRetreat(m_data); partialAmphib = retreatType == RetreatType.PARTIAL_AMPHIB; if (availableTerritories.isEmpty() && !(submerge || canDefendingSubsSubmergeOrRetreat)) { return; } Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; if (subs) { units = Match.getMatches(units, Matches.UnitIsSub); } else if (planes) { units = Match.getMatches(units, Matches.UnitIsAir); } else if (partialAmphib) { units = Match.getMatches(units, Matches.UnitWasNotAmphibious); } if (Match.someMatch(units, Matches.UnitIsSea)) { availableTerritories = Match.getMatches(availableTerritories, Matches.TerritoryIsWater); } if (canDefendingSubsSubmergeOrRetreat) { availableTerritories.add(m_battleSite); } else if (submerge) { availableTerritories.clear(); availableTerritories.add(m_battleSite); } if (planes) { availableTerritories.clear(); availableTerritories.add(m_battleSite); } if (units.size() == 0) { return; } final PlayerID retreatingPlayer = defender ? m_defender : m_attacker; String text; if (subs) { text = retreatingPlayer.getName() + " retreat subs?"; } else if (planes) { text = retreatingPlayer.getName() + RETREAT_PLANES; } else if (partialAmphib) { text = retreatingPlayer.getName() + " retreat non-amphibious units?"; } else { text = retreatingPlayer.getName() + " retreat?"; } String step; if (defender) { step = m_defender.getName() + (canSubsSubmerge ? SUBS_SUBMERGE : SUBS_WITHDRAW); } else { if (subs) { step = m_attacker.getName() + (canSubsSubmerge ? SUBS_SUBMERGE : SUBS_WITHDRAW); } else if (planes) { step = m_attacker.getName() + PLANES_WITHDRAW; } else if (partialAmphib) { step = m_attacker.getName() + NONAMPHIB_WITHDRAW; } else { step = m_attacker.getName() + ATTACKER_WITHDRAW; } } getDisplay(bridge).gotoBattleStep(m_battleID, step); final Territory retreatTo = getRemote(retreatingPlayer, bridge).retreatQuery(m_battleID, (submerge || canDefendingSubsSubmergeOrRetreat), m_battleSite, availableTerritories, text); if (retreatTo != null && !availableTerritories.contains(retreatTo) && !subs) { System.err.println("Invalid retreat selection :" + retreatTo + " not in " + MyFormatter.defaultNamedToTextList(availableTerritories)); Thread.dumpStack(); return; } if (retreatTo != null) { // if attacker retreating non subs then its all over if (!defender && !subs && !planes && !partialAmphib) { // this is illegal in ww2v2 revised and beyond (the fighters should die). still checking if illegal in classic. // ensureAttackingAirCanRetreat(bridge); m_isOver = true; } if (subs && m_battleSite.equals(retreatTo) && (submerge || canDefendingSubsSubmergeOrRetreat)) { if (!m_headless) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_SUBMERGE, m_attacker); } submergeUnits(units, defender, bridge); final String messageShort = retreatingPlayer.getName() + " submerges subs"; getDisplay(bridge).notifyRetreat(messageShort, messageShort, step, retreatingPlayer); } else if (planes) { if (!m_headless) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_AIR, m_attacker); } retreatPlanes(units, defender, bridge); final String messageShort = retreatingPlayer.getName() + " retreats planes"; getDisplay(bridge).notifyRetreat(messageShort, messageShort, step, retreatingPlayer); } else if (partialAmphib) { if (!m_headless) { if (Match.someMatch(units, Matches.UnitIsSea)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_SEA, m_attacker); } else if (Match.someMatch(units, Matches.UnitIsLand)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_LAND, m_attacker); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_AIR, m_attacker); } } // remove amphib units from those retreating units = Match.getMatches(units, Matches.UnitWasNotAmphibious); retreatUnitsAndPlanes(units, retreatTo, defender, bridge); final String messageShort = retreatingPlayer.getName() + " retreats non-amphibious units"; getDisplay(bridge).notifyRetreat(messageShort, messageShort, step, retreatingPlayer); } else { if (!m_headless) { if (Match.someMatch(units, Matches.UnitIsSea)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_SEA, m_attacker); } else if (Match.someMatch(units, Matches.UnitIsLand)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_LAND, m_attacker); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_RETREAT_AIR, m_attacker); } } retreatUnits(units, retreatTo, defender, bridge); final String messageShort = retreatingPlayer.getName() + " retreats"; String messageLong; if (subs) { messageLong = retreatingPlayer.getName() + " retreats subs to " + retreatTo.getName(); } else if (planes) { messageLong = retreatingPlayer.getName() + " retreats planes to " + retreatTo.getName(); } else if (partialAmphib) { messageLong = retreatingPlayer.getName() + " retreats non-amphibious units to " + retreatTo.getName(); } else { messageLong = retreatingPlayer.getName() + " retreats all units to " + retreatTo.getName(); } getDisplay(bridge).notifyRetreat(messageShort, messageLong, step, retreatingPlayer); } } } @Override public List<Unit> getRemainingAttackingUnits() { final ArrayList<Unit> remaining = new ArrayList<>(m_attackingUnits); remaining.addAll(m_attackingUnitsRetreated); return remaining; } @Override public List<Unit> getRemainingDefendingUnits() { final ArrayList<Unit> remaining = new ArrayList<>(m_defendingUnits); remaining.addAll(m_defendingUnitsRetreated); return remaining; } private Change retreatFromDependents(final Collection<Unit> units, final Territory retreatTo, final Collection<IBattle> dependentBattles) { final CompositeChange change = new CompositeChange(); for (final IBattle dependent : dependentBattles) { final Route route = new Route(); route.setStart(m_battleSite); route.add(dependent.getTerritory()); final Collection<Unit> retreatedUnits = dependent.getDependentUnits(units); dependent.removeAttack(route, retreatedUnits); reLoadTransports(units, change); change.add(ChangeFactory.moveUnits(dependent.getTerritory(), retreatTo, retreatedUnits)); } return change; } // Retreat landed units from allied territory when their transport retreats private Change retreatFromNonCombat(Collection<Unit> units, final Territory retreatTo) { final CompositeChange change = new CompositeChange(); units = Match.getMatches(units, Matches.UnitIsTransport); final Collection<Unit> retreated = getTransportDependents(units, m_data); if (!retreated.isEmpty()) { Territory retreatedFrom = null; for (final Unit unit : units) { retreatedFrom = TransportTracker.getTerritoryTransportHasUnloadedTo(unit); if (retreatedFrom != null) { reLoadTransports(units, change); change.add(ChangeFactory.moveUnits(retreatedFrom, retreatTo, retreated)); } } } return change; } public void reLoadTransports(final Collection<Unit> units, final CompositeChange change) { final Collection<Unit> transports = Match.getMatches(units, Matches.UnitCanTransport); // Put units back on their transports for (final Unit transport : transports) { final Collection<Unit> unloaded = TransportTracker.unloaded(transport); for (final Unit load : unloaded) { final Change loadChange = TransportTracker.loadTransportChange((TripleAUnit) transport, load); change.add(loadChange); } } } private void retreatPlanes(final Collection<Unit> retreating, final boolean defender, final IDelegateBridge bridge) { final String transcriptText = MyFormatter.unitsToText(retreating) + " retreated"; final Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; final Collection<Unit> unitsRetreated = defender ? m_defendingUnitsRetreated : m_attackingUnitsRetreated; /** * @todo Does this need to happen with planes retreating too? */ units.removeAll(retreating); unitsRetreated.removeAll(retreating); if (units.isEmpty() || m_isOver) { endBattle(bridge); if (defender) { attackerWins(bridge); } else { defenderWins(bridge); } } else { getDisplay(bridge).notifyRetreat(m_battleID, retreating); } bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(retreating)); } private void submergeUnits(final Collection<Unit> submerging, final boolean defender, final IDelegateBridge bridge) { final String transcriptText = MyFormatter.unitsToText(submerging) + " Submerged"; final Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; final Collection<Unit> unitsRetreated = defender ? m_defendingUnitsRetreated : m_attackingUnitsRetreated; final CompositeChange change = new CompositeChange(); for (final Unit u : submerging) { change.add(ChangeFactory.unitPropertyChange(u, true, TripleAUnit.SUBMERGED)); } bridge.addChange(change); units.removeAll(submerging); unitsRetreated.addAll(submerging); if (!units.isEmpty() && !m_isOver) { getDisplay(bridge).notifyRetreat(m_battleID, submerging); } bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(submerging)); } private void retreatUnits(Collection<Unit> retreating, final Territory to, final boolean defender, final IDelegateBridge bridge) { retreating.addAll(getDependentUnits(retreating)); // our own air units dont retreat with land units final Match<Unit> notMyAir = new CompositeMatchOr<>(Matches.UnitIsNotAir, new InverseMatch<>(Matches.unitIsOwnedBy(m_attacker))); retreating = Match.getMatches(retreating, notMyAir); String transcriptText; // in WW2V1, defending subs can retreat so show owner if (isWW2V2()) { transcriptText = MyFormatter.unitsToTextNoOwner(retreating) + " retreated to " + to.getName(); } else { transcriptText = MyFormatter.unitsToText(retreating) + " retreated to " + to.getName(); } bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(retreating)); final CompositeChange change = new CompositeChange(); change.add(ChangeFactory.moveUnits(m_battleSite, to, retreating)); if (m_isOver) { final Collection<IBattle> dependentBattles = m_battleTracker.getBlocked(this); // If there are no dependent battles, check landings in allied territories if (dependentBattles.isEmpty()) { change.add(retreatFromNonCombat(retreating, to)); // Else retreat the units from combat when their transport retreats } else { change.add(retreatFromDependents(retreating, to, dependentBattles)); } } bridge.addChange(change); final Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; final Collection<Unit> unitsRetreated = defender ? m_defendingUnitsRetreated : m_attackingUnitsRetreated; units.removeAll(retreating); unitsRetreated.addAll(retreating); if (units.isEmpty() || m_isOver) { endBattle(bridge); if (defender) { attackerWins(bridge); } else { defenderWins(bridge); } } else { getDisplay(bridge).notifyRetreat(m_battleID, retreating); } } private void retreatUnitsAndPlanes(final Collection<Unit> retreating, final Territory to, final boolean defender, final IDelegateBridge bridge) { // Remove air from battle final Collection<Unit> units = defender ? m_defendingUnits : m_attackingUnits; final Collection<Unit> unitsRetreated = defender ? m_defendingUnitsRetreated : m_attackingUnitsRetreated; units.removeAll(Match.getMatches(units, Matches.UnitIsAir)); // add all land units' dependents // retreating.addAll(getDependentUnits(retreating)); retreating.addAll(getDependentUnits(units)); // our own air units dont retreat with land units final Match<Unit> notMyAir = new CompositeMatchOr<>(Matches.UnitIsNotAir, new InverseMatch<>(Matches.unitIsOwnedBy(m_attacker))); final Collection<Unit> nonAirRetreating = Match.getMatches(retreating, notMyAir); final String transcriptText = MyFormatter.unitsToTextNoOwner(nonAirRetreating) + " retreated to " + to.getName(); bridge.getHistoryWriter().addChildToEvent(transcriptText, new ArrayList<>(nonAirRetreating)); final CompositeChange change = new CompositeChange(); change.add(ChangeFactory.moveUnits(m_battleSite, to, nonAirRetreating)); if (m_isOver) { final Collection<IBattle> dependentBattles = m_battleTracker.getBlocked(this); // If there are no dependent battles, check landings in allied territories if (dependentBattles.isEmpty()) { change.add(retreatFromNonCombat(nonAirRetreating, to)); // Else retreat the units from combat when their transport retreats } else { change.add(retreatFromDependents(nonAirRetreating, to, dependentBattles)); } } bridge.addChange(change); units.removeAll(nonAirRetreating); unitsRetreated.addAll(nonAirRetreating); if (units.isEmpty() || m_isOver) { endBattle(bridge); if (defender) { attackerWins(bridge); } else { defenderWins(bridge); } } else { getDisplay(bridge).notifyRetreat(m_battleID, retreating); } } private void fire(final String stepName, final Collection<Unit> firingUnits, final Collection<Unit> attackableUnits, final List<Unit> allEnemyUnitsAliveOrWaitingToDie, final boolean defender, final ReturnFire returnFire, final String text) { final PlayerID firing = defender ? m_defender : m_attacker; final PlayerID defending = !defender ? m_defender : m_attacker; if (firingUnits.isEmpty()) { return; } m_stack.push(new Fire(attackableUnits, returnFire, firing, defending, firingUnits, stepName, text, this, defender, m_dependentUnits, m_stack, m_headless, m_battleSite, m_territoryEffects, allEnemyUnitsAliveOrWaitingToDie)); } /** * Check for suicide units and kill them immediately (they get to shoot back, which is the point). */ private void checkSuicideUnits(final IDelegateBridge bridge) { if (isDefendingSuicideAndMunitionUnitsDoNotFire()) { final List<Unit> deadUnits = Match.getMatches(m_attackingUnits, Matches.UnitIsSuicide); getDisplay(bridge).deadUnitNotification(m_battleID, m_attacker, deadUnits, m_dependentUnits); remove(deadUnits, bridge, m_battleSite, false); } else { final List<Unit> deadUnits = new ArrayList<>(); deadUnits.addAll(Match.getMatches(m_defendingUnits, Matches.UnitIsSuicide)); deadUnits.addAll(Match.getMatches(m_attackingUnits, Matches.UnitIsSuicide)); getDisplay(bridge).deadUnitNotification(m_battleID, m_attacker, deadUnits, m_dependentUnits); getDisplay(bridge).deadUnitNotification(m_battleID, m_defender, deadUnits, m_dependentUnits); remove(deadUnits, bridge, m_battleSite, null); } } /** * Check for unescorted TRNS and kill them immediately. */ private void checkUndefendedTransports(final IDelegateBridge bridge, final PlayerID player) { // if we are the attacker, we can retreat instead of dying if (player.equals(m_attacker) && (!getAttackerRetreatTerritories().isEmpty() || Match.someMatch(m_attackingUnits, Matches.UnitIsAir))) { return; } // Get all allied transports in the territory final CompositeMatch<Unit> matchAllied = new CompositeMatchAnd<>(); matchAllied.add(Matches.UnitIsTransport); matchAllied.add(Matches.UnitIsNotCombatTransport); matchAllied.add(Matches.isUnitAllied(player, m_data)); matchAllied.add(Matches.UnitIsSea); final List<Unit> alliedTransports = Match.getMatches(m_battleSite.getUnits().getUnits(), matchAllied); // If no transports, just return if (alliedTransports.isEmpty()) { return; } // Get all ALLIED, sea & air units in the territory (that are NOT submerged) final CompositeMatch<Unit> alliedUnitsMatch = new CompositeMatchAnd<>(); alliedUnitsMatch.add(Matches.isUnitAllied(player, m_data)); alliedUnitsMatch.add(Matches.UnitIsNotLand); alliedUnitsMatch.add(Matches.UnitIsSubmerged.invert()); final Collection<Unit> alliedUnits = Match.getMatches(m_battleSite.getUnits().getUnits(), alliedUnitsMatch); // If transports are unescorted, check opposing forces to see if the Trns die automatically if (alliedTransports.size() == alliedUnits.size()) { // Get all the ENEMY sea and air units (that can attack) in the territory final CompositeMatch<Unit> enemyUnitsMatch = new CompositeMatchAnd<>(); enemyUnitsMatch.add(Matches.UnitIsNotLand); // enemyUnitsMatch.add(Matches.UnitIsNotTransportButCouldBeCombatTransport); enemyUnitsMatch.add(Matches.UnitIsSubmerged.invert()); enemyUnitsMatch.add(Matches.unitCanAttack(player)); final Collection<Unit> enemyUnits = Match.getMatches(m_battleSite.getUnits().getUnits(), enemyUnitsMatch); // If there are attackers set their movement to 0 and kill the transports if (enemyUnits.size() > 0) { final Change change = ChangeFactory.markNoMovementChange(Match.getMatches(enemyUnits, Matches.UnitIsSea)); bridge.addChange(change); final boolean defender = player.equals(m_defender); remove(alliedTransports, bridge, m_battleSite, defender); } } } private void checkForUnitsThatCanRollLeft(final IDelegateBridge bridge, final boolean attacker) { // if we are the attacker, we can retreat instead of dying if (attacker && (!getAttackerRetreatTerritories().isEmpty() || Match.someMatch(m_attackingUnits, Matches.UnitIsAir))) { return; } if (m_attackingUnits.isEmpty() || m_defendingUnits.isEmpty()) { return; } final CompositeMatch<Unit> notSubmergedAndType = new CompositeMatchAnd<>(Matches.UnitIsSubmerged.invert()); if (Matches.TerritoryIsLand.match(m_battleSite)) { notSubmergedAndType.add(Matches.UnitIsSea.invert()); } else { notSubmergedAndType.add(Matches.UnitIsLand.invert()); } final Collection<Unit> unitsToKill; final boolean hasUnitsThatCanRollLeft; if (attacker) { hasUnitsThatCanRollLeft = Match.someMatch(m_attackingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsSupporterOrHasCombatAbility(attacker))); unitsToKill = Match.getMatches(m_attackingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsNotInfrastructure)); } else { hasUnitsThatCanRollLeft = Match.someMatch(m_defendingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsSupporterOrHasCombatAbility(attacker))); unitsToKill = Match.getMatches(m_defendingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsNotInfrastructure)); } final boolean enemy = !attacker; final boolean enemyHasUnitsThatCanRollLeft; if (enemy) { enemyHasUnitsThatCanRollLeft = Match.someMatch(m_attackingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsSupporterOrHasCombatAbility(enemy))); } else { enemyHasUnitsThatCanRollLeft = Match.someMatch(m_defendingUnits, new CompositeMatchAnd<>(notSubmergedAndType, Matches.UnitIsSupporterOrHasCombatAbility(enemy))); } if (!hasUnitsThatCanRollLeft && enemyHasUnitsThatCanRollLeft) { remove(unitsToKill, bridge, m_battleSite, !attacker); } } /** * Submerge attacking/defending SUBS if they're alone OR with TRNS against only AIRCRAFT. */ private void submergeSubsVsOnlyAir(final IDelegateBridge bridge) { // if All attackers are AIR submerge any defending subs ..m_defendingUnits.removeAll(m_killed); if (Match.allMatch(m_attackingUnits, Matches.UnitIsAir) && Match.someMatch(m_defendingUnits, Matches.UnitIsSub)) { // Get all defending subs (including allies) in the territory. final List<Unit> defendingSubs = Match.getMatches(m_defendingUnits, Matches.UnitIsSub); // submerge defending subs submergeUnits(defendingSubs, true, bridge); // checking defending air on attacking subs } else if (Match.allMatch(m_defendingUnits, Matches.UnitIsAir) && Match.someMatch(m_attackingUnits, Matches.UnitIsSub)) { // Get all attacking subs in the territory final List<Unit> attackingSubs = Match.getMatches(m_attackingUnits, Matches.UnitIsSub); // submerge attacking subs submergeUnits(attackingSubs, false, bridge); } } private void defendNonSubs() { if (m_attackingUnits.size() == 0) { return; } Collection<Unit> units = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); units.addAll(m_defendingUnits); units.addAll(m_defendingWaitingToDie); units = Match.getMatches(units, Matches.UnitIsNotSub); // if restricted, remove aircraft from attackers if (isAirAttackSubRestricted() && !canAirAttackSubs(m_attackingUnits, units)) { units.removeAll(Match.getMatches(units, Matches.UnitIsAir)); } if (units.isEmpty()) { return; } final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingWaitingToDie); fire(m_attacker.getName() + SELECT_CASUALTIES, units, m_attackingUnits, allEnemyUnitsAliveOrWaitingToDie, true, ReturnFire.ALL, "Defenders fire, "); } // If there are no attacking DDs but defending SUBs, fire AIR at non-SUB forces ONLY private void attackAirOnNonSubs() { if (m_defendingUnits.size() == 0) { return; } Collection<Unit> units = new ArrayList<>(m_attackingUnits.size() + m_attackingWaitingToDie.size()); units.addAll(m_attackingUnits); units.addAll(m_attackingWaitingToDie); // See if allied air can participate in combat if (!isAlliedAirIndependent()) { units = Match.getMatches(units, Matches.unitIsOwnedBy(m_attacker)); } if (!canAirAttackSubs(m_defendingUnits, units)) { units = Match.getMatches(units, Matches.UnitIsAir); final Collection<Unit> enemyUnitsNotSubs = Match.getMatches(m_defendingUnits, Matches.UnitIsNotSub); final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingWaitingToDie); fire(m_defender.getName() + SELECT_CASUALTIES, units, enemyUnitsNotSubs, allEnemyUnitsAliveOrWaitingToDie, false, ReturnFire.ALL, "Attacker's aircraft fire,"); } } private boolean canAirAttackSubs(final Collection<Unit> firedAt, final Collection<Unit> firing) { return !(m_battleSite.isWater() && Match.someMatch(firedAt, Matches.UnitIsSub) && Match.noneMatch(firing, Matches.UnitIsDestroyer)); } private void defendAirOnNonSubs() { if (m_attackingUnits.size() == 0) { return; } Collection<Unit> units = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); units.addAll(m_defendingUnits); units.addAll(m_defendingWaitingToDie); if (!canAirAttackSubs(m_attackingUnits, units)) { units = Match.getMatches(units, Matches.UnitIsAir); final Collection<Unit> enemyUnitsNotSubs = Match.getMatches(m_attackingUnits, Matches.UnitIsNotSub); if (enemyUnitsNotSubs.isEmpty()) { return; } final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingWaitingToDie); fire(m_attacker.getName() + SELECT_CASUALTIES, units, enemyUnitsNotSubs, allEnemyUnitsAliveOrWaitingToDie, true, ReturnFire.ALL, "Defender's aircraft fire,"); } } // If there are no attacking DDs, but defending SUBs, remove attacking AIR as they've already fired- otherwise fire // all attackers. private void attackNonSubs() { if (m_defendingUnits.size() == 0) { return; } Collection<Unit> units = Match.getMatches(m_attackingUnits, Matches.UnitIsNotSub); units.addAll(Match.getMatches(m_attackingWaitingToDie, Matches.UnitIsNotSub)); // See if allied air can participate in combat if (!isAlliedAirIndependent()) { units = Match.getMatches(units, Matches.unitIsOwnedBy(m_attacker)); } // if restricted, remove aircraft from attackers if (isAirAttackSubRestricted() && !canAirAttackSubs(m_defendingUnits, units)) { units.removeAll(Match.getMatches(units, Matches.UnitIsAir)); } if (units.isEmpty()) { return; } final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingWaitingToDie); fire(m_defender.getName() + SELECT_CASUALTIES, units, m_defendingUnits, allEnemyUnitsAliveOrWaitingToDie, false, ReturnFire.ALL, "Attackers fire,"); } private void attackSubs(final ReturnFire returnFire) { final Collection<Unit> firing = Match.getMatches(m_attackingUnits, Matches.UnitIsSub); if (firing.isEmpty()) { return; } final Collection<Unit> attacked = Match.getMatches(m_defendingUnits, Matches.UnitIsNotAir); // if there are destroyers in the attacked units, we can return fire. final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingWaitingToDie); fire(m_defender.getName() + SELECT_SUB_CASUALTIES, firing, attacked, allEnemyUnitsAliveOrWaitingToDie, false, returnFire, "Subs fire,"); } private void defendSubs(final ReturnFire returnFire) { if (m_attackingUnits.size() == 0) { return; } Collection<Unit> firing = new ArrayList<>(m_defendingUnits.size() + m_defendingWaitingToDie.size()); firing.addAll(m_defendingUnits); firing.addAll(m_defendingWaitingToDie); firing = Match.getMatches(firing, Matches.UnitIsSub); if (firing.isEmpty()) { return; } final Collection<Unit> attacked = Match.getMatches(m_attackingUnits, Matches.UnitIsNotAir); if (attacked.isEmpty()) { return; } final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingWaitingToDie); fire(m_attacker.getName() + SELECT_SUB_CASUALTIES, firing, attacked, allEnemyUnitsAliveOrWaitingToDie, true, returnFire, "Subs defend, "); } void removeCasualties(final Collection<Unit> killed, final ReturnFire returnFire, final boolean defender, final IDelegateBridge bridge, final boolean isAA) { if (killed.isEmpty()) { return; } if (returnFire == ReturnFire.ALL) { // move to waiting to die if (defender) { m_defendingWaitingToDie.addAll(killed); } else { m_attackingWaitingToDie.addAll(killed); } } else if (returnFire == ReturnFire.SUBS) { // move to waiting to die if (defender) { m_defendingWaitingToDie.addAll(Match.getMatches(killed, Matches.UnitIsSub)); } else { m_attackingWaitingToDie.addAll(Match.getMatches(killed, Matches.UnitIsSub)); } remove(Match.getMatches(killed, Matches.UnitIsNotSub), bridge, m_battleSite, defender); } else if (returnFire == ReturnFire.NONE) { remove(killed, bridge, m_battleSite, defender); } // remove from the active fighting if (defender) { m_defendingUnits.removeAll(killed); } else { m_attackingUnits.removeAll(killed); } } private void fireNavalBombardment(final IDelegateBridge bridge) { // TODO - check within the method for the bombarding limitations final Collection<Unit> bombard = getBombardingUnits(); final Collection<Unit> attacked = Match.getMatches(m_defendingUnits, Matches.UnitIsNotInfrastructureAndNotCapturedOnEntering(m_attacker, m_battleSite, m_data)); // bombarding units cant move after bombarding if (!m_headless) { final Change change = ChangeFactory.markNoMovementChange(bombard); bridge.addChange(change); } /** * TODO This code is actually a bug- the property is intended to tell if the return fire is * RESTRICTED- but it's used as if it's ALLOWED. The reason is the default values on the * property definition. However, fixing this will entail a fix to the XML to reverse * all values. We'll leave it as is for now and try to figure out a patch strategy later. */ final boolean canReturnFire = (isNavalBombardCasualtiesReturnFire()); if (bombard.size() > 0 && attacked.size() > 0) { if (!m_headless) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_BOMBARD, m_attacker); } final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingWaitingToDie); fire(SELECT_NAVAL_BOMBARDMENT_CASUALTIES, bombard, attacked, allEnemyUnitsAliveOrWaitingToDie, false, canReturnFire ? ReturnFire.ALL : ReturnFire.NONE, "Bombard"); } } private void fireSuicideUnitsAttack() { // TODO: add a global toggle for returning fire (Veqryn) final CompositeMatch<Unit> attackableUnits = new CompositeMatchAnd<>( Matches.UnitIsNotInfrastructureAndNotCapturedOnEntering(m_attacker, m_battleSite, m_data), Matches.UnitIsSuicide.invert(), Matches.unitIsBeingTransported().invert()); final Collection<Unit> suicideAttackers = Match.getMatches(m_attackingUnits, Matches.UnitIsSuicide); final Collection<Unit> attackedDefenders = Match.getMatches(m_defendingUnits, attackableUnits); // comparatively simple rules for isSuicide units. if AirAttackSubRestricted and you have no destroyers, you can't // attack subs with // anything. if (isAirAttackSubRestricted() && !Match.someMatch(m_attackingUnits, Matches.UnitIsDestroyer) && Match.someMatch(attackedDefenders, Matches.UnitIsSub)) { attackedDefenders.removeAll(Match.getMatches(attackedDefenders, Matches.UnitIsSub)); } if (Match.allMatch(suicideAttackers, Matches.UnitIsSub)) { attackedDefenders.removeAll(Match.getMatches(attackedDefenders, Matches.UnitIsAir)); } if (suicideAttackers.size() == 0 || attackedDefenders.size() == 0) { return; } final boolean canReturnFire = (!isSuicideAndMunitionCasualtiesRestricted()); final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_defendingWaitingToDie); fire(m_defender.getName() + SELECT_CASUALTIES_SUICIDE, suicideAttackers, attackedDefenders, allEnemyUnitsAliveOrWaitingToDie, false, canReturnFire ? ReturnFire.ALL : ReturnFire.NONE, SUICIDE_ATTACK); } private void fireSuicideUnitsDefend() { if (isDefendingSuicideAndMunitionUnitsDoNotFire()) { return; } // TODO: add a global toggle for returning fire (Veqryn) final CompositeMatch<Unit> attackableUnits = new CompositeMatchAnd<>(Matches.UnitIsNotInfrastructure, Matches.UnitIsSuicide.invert(), Matches.unitIsBeingTransported().invert()); final Collection<Unit> suicideDefenders = Match.getMatches(m_defendingUnits, Matches.UnitIsSuicide); final Collection<Unit> attackedAttackers = Match.getMatches(m_attackingUnits, attackableUnits); // comparatively simple rules for isSuicide units. if AirAttackSubRestricted and you have no destroyers, you can't // attack subs with // anything. if (isAirAttackSubRestricted() && !Match.someMatch(m_defendingUnits, Matches.UnitIsDestroyer) && Match.someMatch(attackedAttackers, Matches.UnitIsSub)) { attackedAttackers.removeAll(Match.getMatches(attackedAttackers, Matches.UnitIsSub)); } if (Match.allMatch(suicideDefenders, Matches.UnitIsSub)) { suicideDefenders.removeAll(Match.getMatches(suicideDefenders, Matches.UnitIsAir)); } if (suicideDefenders.size() == 0 || attackedAttackers.size() == 0) { return; } final boolean canReturnFire = (!isSuicideAndMunitionCasualtiesRestricted()); final List<Unit> allEnemyUnitsAliveOrWaitingToDie = new ArrayList<>(); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingUnits); allEnemyUnitsAliveOrWaitingToDie.addAll(m_attackingWaitingToDie); fire(m_attacker.getName() + SELECT_CASUALTIES_SUICIDE, suicideDefenders, attackedAttackers, allEnemyUnitsAliveOrWaitingToDie, true, canReturnFire ? ReturnFire.ALL : ReturnFire.NONE, SUICIDE_DEFEND); } private boolean isWW2V2() { return games.strategy.triplea.Properties.getWW2V2(m_data); } private boolean isWW2V3() { return games.strategy.triplea.Properties.getWW2V3(m_data); } private boolean isPartialAmphibiousRetreat() { return games.strategy.triplea.Properties.getPartialAmphibiousRetreat(m_data); } private boolean isAlliedAirIndependent() { return games.strategy.triplea.Properties.getAlliedAirIndependent(m_data); } private boolean isDefendingSubsSneakAttack() { return games.strategy.triplea.Properties.getDefendingSubsSneakAttack(m_data); } private boolean isAttackerRetreatPlanes() { return games.strategy.triplea.Properties.getAttackerRetreatPlanes(m_data); } private boolean isNavalBombardCasualtiesReturnFire() { return games.strategy.triplea.Properties.getNavalBombardCasualtiesReturnFireRestricted(m_data); } private boolean isSuicideAndMunitionCasualtiesRestricted() { return games.strategy.triplea.Properties.getSuicideAndMunitionCasualtiesRestricted(m_data); } private boolean isDefendingSuicideAndMunitionUnitsDoNotFire() { return games.strategy.triplea.Properties.getDefendingSuicideAndMunitionUnitsDoNotFire(m_data); } private boolean isAirAttackSubRestricted() { return games.strategy.triplea.Properties.getAirAttackSubRestricted(m_data); } private boolean isSubRetreatBeforeBattle() { return games.strategy.triplea.Properties.getSubRetreatBeforeBattle(m_data); } private boolean isTransportCasualtiesRestricted() { return games.strategy.triplea.Properties.getTransportCasualtiesRestricted(m_data); } private void fireOffensiveAAGuns() { m_stack.push(new FireAA(false)); } private void fireDefensiveAAGuns() { m_stack.push(new FireAA(true)); } class FireAA implements IExecutable { private static final long serialVersionUID = -6406659798754841382L; private final boolean m_defending; private DiceRoll m_dice; private CasualtyDetails m_casualties; Collection<Unit> m_casualtiesSoFar = new ArrayList<>(); private FireAA(final boolean defending) { m_defending = defending; } @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if ((m_defending && !canFireDefendingAA()) || (!m_defending && !canFireOffensiveAA())) { return; } for (final String currentTypeAA : (m_defending ? m_defendingAAtypes : m_offensiveAAtypes)) { final Collection<Unit> currentPossibleAA = Match.getMatches((m_defending ? m_defendingAA : m_offensiveAA), Matches.UnitIsAAofTypeAA(currentTypeAA)); final Set<UnitType> targetUnitTypesForThisTypeAA = UnitAttachment.get(currentPossibleAA.iterator().next().getType()).getTargetsAA(m_data); final Set<UnitType> airborneTypesTargettedToo = m_defending ? TechAbilityAttachment.getAirborneTargettedByAA(m_attacker, m_data).get(currentTypeAA) : new HashSet<>(); final Collection<Unit> validAttackingUnitsForThisRoll = Match.getMatches((m_defending ? m_attackingUnits : m_defendingUnits), new CompositeMatchOr<>(Matches .unitIsOfTypes(targetUnitTypesForThisTypeAA), new CompositeMatchAnd<Unit>(Matches.UnitIsAirborne, Matches.unitIsOfTypes(airborneTypesTargettedToo)))); final IExecutable rollDice = new IExecutable() { private static final long serialVersionUID = 6435935558879109347L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { validAttackingUnitsForThisRoll.removeAll(m_casualtiesSoFar); if (!validAttackingUnitsForThisRoll.isEmpty()) { m_dice = DiceRoll.rollAA(validAttackingUnitsForThisRoll, currentPossibleAA, bridge, m_battleSite, m_defending); if (!m_headless) { if (currentTypeAA.equals("AA")) { if (m_dice.getHits() > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_HIT, (m_defending ? m_defender : m_attacker)); } else { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AA_MISS, (m_defending ? m_defender : m_attacker)); } } else { if (m_dice.getHits() > 0) { bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_HIT, (m_defending ? m_defender : m_attacker)); } else { bridge.getSoundChannelBroadcaster().playSoundForAll( SoundPath.CLIP_BATTLE_X_PREFIX + currentTypeAA.toLowerCase() + SoundPath.CLIP_BATTLE_X_MISS, (m_defending ? m_defender : m_attacker)); } } } } } }; final IExecutable selectCasualties = new IExecutable() { private static final long serialVersionUID = 7943295620796835166L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validAttackingUnitsForThisRoll.isEmpty()) { final CasualtyDetails details = selectCasualties(validAttackingUnitsForThisRoll, currentPossibleAA, bridge, currentTypeAA); markDamaged(details.getDamaged(), bridge); m_casualties = details; m_casualtiesSoFar.addAll(details.getKilled()); } } }; final IExecutable notifyCasualties = new IExecutable() { private static final long serialVersionUID = -6759782085212899725L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { if (!validAttackingUnitsForThisRoll.isEmpty()) { notifyCasualtiesAA(bridge, currentTypeAA); removeCasualties(m_casualties.getKilled(), ReturnFire.ALL, !m_defending, bridge, m_defending); } } }; // push in reverse order of execution stack.push(notifyCasualties); stack.push(selectCasualties); stack.push(rollDice); } } private CasualtyDetails selectCasualties(final Collection<Unit> validAttackingUnitsForThisRoll, final Collection<Unit> defendingAA, final IDelegateBridge bridge, final String currentTypeAA) { // send defender the dice roll so he can see what the dice are while he waits for attacker to select casualties getDisplay(bridge).notifyDice(m_dice, (m_defending ? m_attacker.getName() : m_defender.getName()) + SELECT_PREFIX + currentTypeAA + CASUALTIES_SUFFIX); return BattleCalculator.getAACasualties(!m_defending, validAttackingUnitsForThisRoll, (m_defending ? m_attackingUnits : m_defendingUnits), defendingAA, (m_defending ? m_defendingUnits : m_attackingUnits), m_dice, bridge, (m_defending ? m_defender : m_attacker), (m_defending ? m_attacker : m_defender), m_battleID, m_battleSite, m_territoryEffects, m_isAmphibious, m_amphibiousLandAttackers); } private void notifyCasualtiesAA(final IDelegateBridge bridge, final String currentTypeAA) { if (m_headless) { return; } getDisplay(bridge).casualtyNotification(m_battleID, (m_defending ? m_attacker.getName() : m_defender.getName()) + REMOVE_PREFIX + currentTypeAA + CASUALTIES_SUFFIX, m_dice, (m_defending ? m_attacker : m_defender), new ArrayList<>(m_casualties.getKilled()), new ArrayList<>(m_casualties.getDamaged()), m_dependentUnits); getRemote((m_defending ? m_attacker : m_defender), bridge).confirmOwnCasualties(m_battleID, "Press space to continue"); final Runnable r = () -> { try { getRemote((m_defending ? m_defender : m_attacker), bridge).confirmEnemyCasualties(m_battleID, "Press space to continue", (m_defending ? m_attacker : m_defender)); } catch (final ConnectionLostException cle) { // somone else will deal with this // System.out.println(cle.getMessage()); // cle.printStackTrace(System.out); } catch (final Exception e) { // ignore } }; final Thread t = new Thread(r, "click to continue waiter"); t.start(); try { bridge.leaveDelegateExecution(); t.join(); } catch (final InterruptedException e) { // ignore } finally { bridge.enterDelegateExecution(); } } } private boolean canFireDefendingAA() { if (m_defendingAA == null) { updateDefendingAAUnits(); } return m_defendingAA.size() > 0; } private boolean canFireOffensiveAA() { if (m_offensiveAA == null) { updateOffensiveAAUnits(); } return m_offensiveAA.size() > 0; } /** * @return a collection containing all the combatants in units non * combatants include such things as factories, aaguns, land units * in a water battle. */ private List<Unit> removeNonCombatants(final Collection<Unit> units, final boolean attacking, final boolean doNotIncludeAA, final boolean doNotIncludeSeaBombardmentUnits, final boolean removeForNextRound) { final List<Unit> unitList = new ArrayList<>(units); if (m_battleSite.isWater()) { unitList.removeAll(Match.getMatches(unitList, Matches.UnitIsLand)); } // still allow infrastructure type units that can provide support have combat abilities // remove infrastructure units that can't take part in combat (air/naval bases, etc...) unitList.removeAll(Match.getMatches(unitList, Matches.UnitCanBeInBattle(attacking, !m_battleSite.isWater(), (removeForNextRound ? m_round + 1 : m_round), true, doNotIncludeAA, doNotIncludeSeaBombardmentUnits) .invert())); // remove any disabled units from combat unitList.removeAll(Match.getMatches(unitList, Matches.UnitIsDisabled)); // remove capturableOnEntering units (veqryn) unitList.removeAll(Match.getMatches(unitList, Matches.UnitCanBeCapturedOnEnteringToInThisTerritory(m_attacker, m_battleSite, m_data))); // remove any allied air units that are stuck on damaged carriers (veqryn) unitList.removeAll(Match.getMatches(unitList, new CompositeMatchAnd<>(Matches.unitIsBeingTransported(), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier))); // remove any units that were in air combat (veqryn) unitList.removeAll(Match.getMatches(unitList, Matches.UnitWasInAirBattle)); return unitList; } private void removeNonCombatants(final IDelegateBridge bridge, final boolean doNotIncludeAA, final boolean doNotIncludeSeaBombardmentUnits, final boolean removeForNextRound) { final List<Unit> notRemovedDefending = removeNonCombatants(m_defendingUnits, false, doNotIncludeAA, doNotIncludeSeaBombardmentUnits, removeForNextRound); final List<Unit> notRemovedAttacking = removeNonCombatants(m_attackingUnits, true, doNotIncludeAA, doNotIncludeSeaBombardmentUnits, removeForNextRound); final Collection<Unit> toRemoveDefending = Util.difference(m_defendingUnits, notRemovedDefending); final Collection<Unit> toRemoveAttacking = Util.difference(m_attackingUnits, notRemovedAttacking); m_defendingUnits = notRemovedDefending; m_attackingUnits = notRemovedAttacking; if (!m_headless) { if (!toRemoveDefending.isEmpty()) { getDisplay(bridge).changedUnitsNotification(m_battleID, m_defender, toRemoveDefending, null, null); } if (!toRemoveAttacking.isEmpty()) { getDisplay(bridge).changedUnitsNotification(m_battleID, m_attacker, toRemoveAttacking, null, null); } } } private void landParatroops(final IDelegateBridge bridge) { if (TechAttachment.isAirTransportable(m_attacker)) { final Collection<Unit> airTransports = Match.getMatches(m_battleSite.getUnits().getUnits(), Matches.UnitIsAirTransport); if (!airTransports.isEmpty()) { final Collection<Unit> dependents = getDependentUnits(airTransports); if (!dependents.isEmpty()) { final Iterator<Unit> dependentsIter = dependents.iterator(); final CompositeChange change = new CompositeChange(); // remove dependency from paratroops // unload the transports while (dependentsIter.hasNext()) { final Unit unit = dependentsIter.next(); change.add(TransportTracker.unloadAirTransportChange((TripleAUnit) unit, m_battleSite, m_attacker, false)); } bridge.addChange(change); // remove bombers from m_dependentUnits for (final Unit unit : airTransports) { m_dependentUnits.remove(unit); } } } } } private void markNoMovementLeft(final IDelegateBridge bridge) { if (m_headless) { return; } final Collection<Unit> attackingNonAir = Match.getMatches(m_attackingUnits, Matches.UnitIsAir.invert()); final Change noMovementChange = ChangeFactory.markNoMovementChange(attackingNonAir); if (!noMovementChange.isEmpty()) { bridge.addChange(noMovementChange); } } // Figure out what units a transport is transported and has unloaded public Collection<Unit> getTransportDependents(final Collection<Unit> targets, final GameData data) { if (m_headless) { return Collections.emptyList(); } final Collection<Unit> dependents = new ArrayList<>(); if (Match.someMatch(targets, Matches.UnitCanTransport)) { // just worry about transports for (final Unit target : targets) { dependents.addAll(TransportTracker.transportingAndUnloaded(target)); } } return dependents; } private void remove(final Collection<Unit> killed, final IDelegateBridge bridge, final Territory battleSite, final Boolean defenderDying) { 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(this); // If there are NO dependent battles, check for unloads in allied territories if (dependentBattles.isEmpty()) { removeFromNonCombatLandings(killed, bridge); // otherwise remove them and the units involved } else { removeFromDependents(killed, bridge, dependentBattles); } // and remove them from the battle display if (defenderDying == null || defenderDying) { m_defendingUnits.removeAll(killed); } if (defenderDying == null || !defenderDying) { m_attackingUnits.removeAll(killed); } } private void removeFromDependents(final Collection<Unit> units, final IDelegateBridge bridge, final Collection<IBattle> dependents) { for (final IBattle dependent : dependents) { dependent.unitsLostInPrecedingBattle(this, units, bridge, false); } } // Remove landed units from allied territory when their transport sinks private void removeFromNonCombatLandings(final Collection<Unit> units, final IDelegateBridge bridge) { for (final Unit transport : Match.getMatches(units, Matches.UnitIsTransport)) { final Collection<Unit> lost = getTransportDependents(Collections.singleton(transport), m_data); if (lost.isEmpty()) { continue; } final Territory landedTerritory = TransportTracker.getTerritoryTransportHasUnloadedTo(transport); if (landedTerritory == null) { throw new IllegalStateException("not unloaded?:" + units); } remove(lost, bridge, landedTerritory, false); } } private void clearWaitingToDie(final IDelegateBridge bridge) { final Collection<Unit> units = new ArrayList<>(); units.addAll(m_attackingWaitingToDie); units.addAll(m_defendingWaitingToDie); remove(units, bridge, m_battleSite, null); m_defendingWaitingToDie.clear(); m_attackingWaitingToDie.clear(); } private void defenderWins(final IDelegateBridge bridge) { m_whoWon = WhoWon.DEFENDER; getDisplay(bridge).battleEnd(m_battleID, m_defender.getName() + " win"); if (games.strategy.triplea.Properties.getAbandonedTerritoriesMayBeTakenOverImmediately(m_data)) { if (Match.getMatches(m_defendingUnits, Matches.UnitIsNotInfrastructure).size() == 0) { final List<Unit> allyOfAttackerUnits = m_battleSite.getUnits().getMatches(Matches.UnitIsNotInfrastructure); if (!allyOfAttackerUnits.isEmpty()) { final PlayerID abandonedToPlayer = AbstractBattle.findPlayerWithMostUnits(allyOfAttackerUnits); bridge.getHistoryWriter() .addChildToEvent( abandonedToPlayer.getName() + " takes over " + m_battleSite.getName() + " as there are no defenders left", allyOfAttackerUnits); // should we create a new battle records to show the ally capturing the territory (in the case where they // didn't already own/allied it)? m_battleTracker.takeOver(m_battleSite, abandonedToPlayer, bridge, null, allyOfAttackerUnits); } } else { // should we create a new battle records to show the defender capturing the territory (in the case where they // didn't already own/allied it)? m_battleTracker.takeOver(m_battleSite, m_defender, bridge, null, m_defendingUnits); } } bridge.getHistoryWriter().addChildToEvent(m_defender.getName() + " win", new ArrayList<>(m_defendingUnits)); m_battleResultDescription = BattleRecord.BattleResultDescription.LOST; showCasualties(bridge); if (!m_headless) { m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); } checkDefendingPlanesCanLand(); BattleTracker.captureOrDestroyUnits(m_battleSite, m_defender, m_defender, bridge, null); if (!m_headless) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_FAILURE, m_attacker); } } private void nobodyWins(final IDelegateBridge bridge) { m_whoWon = WhoWon.DRAW; getDisplay(bridge).battleEnd(m_battleID, "Stalemate"); bridge.getHistoryWriter() .addChildToEvent(m_defender.getName() + " and " + m_attacker.getName() + " reach a stalemate"); m_battleResultDescription = BattleRecord.BattleResultDescription.STALEMATE; showCasualties(bridge); if (!m_headless) { m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_STALEMATE, m_attacker); } checkDefendingPlanesCanLand(); } private void attackerWins(final IDelegateBridge bridge) { m_whoWon = WhoWon.ATTACKER; getDisplay(bridge).battleEnd(m_battleID, m_attacker.getName() + " win"); if (m_headless) { return; } // do we need to change ownership if (Match.someMatch(m_attackingUnits, Matches.UnitIsNotAir)) { if (Matches.isTerritoryEnemyAndNotUnownedWater(m_attacker, m_data).match(m_battleSite)) { m_battleTracker.addToConquered(m_battleSite); } m_battleTracker.takeOver(m_battleSite, m_attacker, bridge, null, m_attackingUnits); m_battleResultDescription = BattleRecord.BattleResultDescription.CONQUERED; } else { m_battleResultDescription = BattleRecord.BattleResultDescription.WON_WITHOUT_CONQUERING; } // Clear the transported_by for successfully offloaded units final Collection<Unit> transports = Match.getMatches(m_attackingUnits, Matches.UnitIsTransport); if (!transports.isEmpty()) { final CompositeChange change = new CompositeChange(); final Collection<Unit> dependents = getTransportDependents(transports, m_data); if (!dependents.isEmpty()) { for (final Unit unit : dependents) { // clear the loaded by ONLY for Combat unloads. NonCombat unloads are handled elsewhere. if (Matches.UnitWasUnloadedThisTurn.match(unit)) { change.add(ChangeFactory.unitPropertyChange(unit, null, TripleAUnit.TRANSPORTED_BY)); } } bridge.addChange(change); } } bridge.getHistoryWriter().addChildToEvent(m_attacker.getName() + " win", new ArrayList<>(m_attackingUnits)); showCasualties(bridge); if (!m_headless) { m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, m_battleResultDescription, new BattleResults(this, m_data)); } if (!m_headless) { if (Matches.TerritoryIsWater.match(m_battleSite)) { if (Match.allMatch(m_attackingUnits, Matches.UnitIsAir)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR_SUCCESSFUL, m_attacker); } else { // assume some naval bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_SEA_SUCCESSFUL, m_attacker); } } else { // no sounds for a successful land battle, because land battle means we are going to capture a territory, and we // have capture sounds // for that if (Match.allMatch(m_attackingUnits, Matches.UnitIsAir)) { bridge.getSoundChannelBroadcaster().playSoundForAll(SoundPath.CLIP_BATTLE_AIR_SUCCESSFUL, m_attacker); } } } } /** * The defender has won, but there may be defending fighters that cant stay * in the sea zone due to insufficient carriers. */ private void checkDefendingPlanesCanLand() { if (m_headless) { return; } // not water, not relevant. if (!m_battleSite.isWater()) { return; } // TODO: why do we keep checking throughout this entire class if the units in m_defendingUnits are allied with // defender, and if the // units in m_attackingUnits are allied with the attacker? Does it really matter? final CompositeMatch<Unit> alliedDefendingAir = new CompositeMatchAnd<>(Matches.UnitIsAir, Matches.UnitWasScrambled.invert()); final Collection<Unit> m_defendingAir = Match.getMatches(m_defendingUnits, alliedDefendingAir); // no planes, exit if (m_defendingAir.isEmpty()) { return; } int carrierCost = AirMovementValidator.carrierCost(m_defendingAir); final int carrierCapacity = AirMovementValidator.carrierCapacity(m_defendingUnits, m_battleSite); // add dependant air to carrier cost carrierCost += AirMovementValidator.carrierCost(Match.getMatches(getDependentUnits(m_defendingUnits), alliedDefendingAir)); // all planes can land, exit if (carrierCapacity >= carrierCost) { return; } // find out what we must remove // remove all the air that can land on carriers from defendingAir carrierCost = 0; // add dependant air to carrier cost carrierCost += AirMovementValidator.carrierCost(Match.getMatches(getDependentUnits(m_defendingUnits), alliedDefendingAir)); for (final Unit currentUnit : new ArrayList<>(m_defendingAir)) { if (!Matches.UnitCanLandOnCarrier.match(currentUnit)) { m_defendingAir.remove(currentUnit); continue; } carrierCost += UnitAttachment.get(currentUnit.getType()).getCarrierCost(); if (carrierCapacity >= carrierCost) { m_defendingAir.remove(currentUnit); } } // Moved this choosing to after all battles, as we legally should be able to land in a territory if we win there. m_battleTracker.addToDefendingAirThatCanNotLand(m_defendingAir, m_battleSite); } public static CompositeChange clearTransportedByForAlliedAirOnCarrier(final Collection<Unit> attackingUnits, final Territory battleSite, final PlayerID attacker, final GameData data) { final CompositeChange change = new CompositeChange(); // Clear the transported_by for successfully won battles where there was an allied air unit held as cargo by an // carrier unit final Collection<Unit> carriers = Match.getMatches(attackingUnits, Matches.UnitIsCarrier); if (!carriers.isEmpty() && !games.strategy.triplea.Properties.getAlliedAirIndependent(data)) { final Match<Unit> alliedFighters = new CompositeMatchAnd<>(Matches.isUnitAllied(attacker, data), Matches.unitIsOwnedBy(attacker).invert(), Matches.UnitIsAir, Matches.UnitCanLandOnCarrier); final Collection<Unit> alliedAirInTerr = Match.getMatches(battleSite.getUnits().getUnits(), alliedFighters); for (final Unit fighter : alliedAirInTerr) { final TripleAUnit taUnit = (TripleAUnit) fighter; if (taUnit.getTransportedBy() != null) { final Unit carrierTransportingThisUnit = taUnit.getTransportedBy(); if (!Matches.UnitHasWhenCombatDamagedEffect(UnitAttachment.UNITSMAYNOTLEAVEALLIEDCARRIER) .match(carrierTransportingThisUnit)) { change.add(ChangeFactory.unitPropertyChange(fighter, null, TripleAUnit.TRANSPORTED_BY)); } } } } return change; } private void showCasualties(final IDelegateBridge bridge) { if (m_killed.isEmpty()) { return; } // a handy summary of all the units killed IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(m_attacker, m_data); final int tuvLostAttacker = BattleCalculator.getTUV(m_killed, m_attacker, costs, m_data); costs = BattleCalculator.getCostsForTUV(m_defender, m_data); final int tuvLostDefender = BattleCalculator.getTUV(m_killed, m_defender, costs, m_data); final int tuvChange = tuvLostDefender - tuvLostAttacker; bridge.getHistoryWriter().addChildToEvent( "Battle casualty summary: Battle score (TUV change) for attacker is " + tuvChange, new ArrayList<>(m_killed)); m_attackerLostTUV += tuvLostAttacker; m_defenderLostTUV += tuvLostDefender; } private void endBattle(final IDelegateBridge bridge) { clearWaitingToDie(bridge); m_isOver = true; m_battleTracker.removeBattle(this); final CompositeChange clearAlliedAir = clearTransportedByForAlliedAirOnCarrier(m_attackingUnits, m_battleSite, m_attacker, m_data); if (!clearAlliedAir.isEmpty()) { bridge.addChange(clearAlliedAir); } } @Override public void cancelBattle(final IDelegateBridge bridge) { endBattle(bridge); } @Override public String toString() { return "Battle in:" + m_battleSite + " battle type:" + m_battleType + " defender:" + m_defender.getName() + " attacked by:" + m_attacker.getName() + " from:" + m_attackingFrom + " attacking with: " + m_attackingUnits; } // In an amphibious assault, sort on who is unloading from xports first // This will allow the marines with higher scores to get killed last public void sortAmphib(final List<Unit> units, final GameData data) { final Comparator<Unit> decreasingMovement = UnitComparator.getLowestToHighestMovementComparator(); final Comparator<Unit> comparator = (u1, u2) -> { int amphibComp = 0; if (u1.getUnitType().equals(u2.getUnitType())) { final UnitAttachment ua = UnitAttachment.get(u1.getType()); final UnitAttachment ua2 = UnitAttachment.get(u2.getType()); if (ua.getIsMarine() != 0 && ua2.getIsMarine() != 0) { amphibComp = compareAccordingToAmphibious(u1, u2); } if (amphibComp == 0) { return decreasingMovement.compare(u1, u2); } return amphibComp; } return u1.getUnitType().getName().compareTo(u2.getUnitType().getName()); }; Collections.sort(units, comparator); } private int compareAccordingToAmphibious(final Unit u1, final Unit u2) { if (m_amphibiousLandAttackers.contains(u1) && !m_amphibiousLandAttackers.contains(u2)) { return -1; } else if (m_amphibiousLandAttackers.contains(u2) && !m_amphibiousLandAttackers.contains(u1)) { return 1; } final int m1 = UnitAttachment.get(u1.getType()).getIsMarine(); final int m2 = UnitAttachment.get(u2.getType()).getIsMarine(); return m2 - m1; } // used for setting stuff when we make a scrambling battle when there was no previous battle there, and we need // retreat spaces public void setAttackingFromAndMap(final Map<Territory, Collection<Unit>> attackingFromMap) { m_attackingFromMap = attackingFromMap; m_attackingFrom = new HashSet<>(attackingFromMap.keySet()); } @Override public void unitsLostInPrecedingBattle(final IBattle battle, final Collection<Unit> units, final IDelegateBridge bridge, final boolean withdrawn) { Collection<Unit> lost = getDependentUnits(units); lost.addAll(Util.intersection(units, m_attackingUnits)); // if all the amphibious attacking land units are lost, then we are // no longer a naval invasion m_amphibiousLandAttackers.removeAll(lost); if (m_amphibiousLandAttackers.isEmpty()) { m_isAmphibious = false; m_bombardingUnits.clear(); } m_attackingUnits.removeAll(lost); // now that they are definitely removed from our attacking list, make sure that they were not already removed from // the territory by the // previous battle's remove method lost = Match.getMatches(lost, Matches.unitIsInTerritory(m_battleSite)); if (!withdrawn) { remove(lost, bridge, m_battleSite, false); } if (m_attackingUnits.isEmpty()) { final IntegerMap<UnitType> costs = BattleCalculator.getCostsForTUV(m_attacker, m_data); final int tuvLostAttacker = (withdrawn ? 0 : BattleCalculator.getTUV(lost, m_attacker, costs, m_data)); m_attackerLostTUV += tuvLostAttacker; m_whoWon = WhoWon.DEFENDER; if (!m_headless) { m_battleTracker.getBattleRecords().addResultToBattle(m_attacker, m_battleID, m_defender, m_attackerLostTUV, m_defenderLostTUV, BattleRecord.BattleResultDescription.LOST, new BattleResults(this, m_data)); } m_battleTracker.removeBattle(this); } } /** * Returns a map of transport -> collection of transported units. */ private Map<Unit, Collection<Unit>> transporting(final Collection<Unit> units) { return TransportTracker.transporting(units); } }