package games.strategy.triplea.delegate; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Territory; import games.strategy.engine.data.TerritoryEffect; import games.strategy.engine.data.Unit; import games.strategy.engine.delegate.IDelegateBridge; import games.strategy.engine.message.ConnectionLostException; import games.strategy.net.GUID; import games.strategy.triplea.delegate.dataObjects.CasualtyDetails; import games.strategy.util.CompositeMatch; import games.strategy.util.CompositeMatchAnd; import games.strategy.util.CompositeMatchOr; import games.strategy.util.Match; public class Fire implements IExecutable { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -3687054738070722403L; private final String m_stepName; private final Collection<Unit> m_firingUnits; private final Collection<Unit> m_attackableUnits; private final MustFightBattle.ReturnFire m_canReturnFire; private final String m_text; private final MustFightBattle m_battle; private final PlayerID m_firingPlayer; private final PlayerID m_hitPlayer; private final boolean m_defending; private final Map<Unit, Collection<Unit>> m_dependentUnits; private final GUID m_battleID; private DiceRoll m_dice; private Collection<Unit> m_killed; private Collection<Unit> m_damaged; private boolean m_confirmOwnCasualties = true; private final boolean m_isHeadless; private final Territory m_battleSite; private final Collection<TerritoryEffect> m_territoryEffects; private final List<Unit> m_allEnemyUnitsAliveOrWaitingToDie; private final Collection<Unit> m_allFriendlyUnitsNotIncludingWaitingToDie; private final Collection<Unit> m_allEnemyUnitsNotIncludingWaitingToDie; private final boolean m_isAmphibious; private final Collection<Unit> m_amphibiousLandAttackers; public Fire(final Collection<Unit> attackableUnits, final MustFightBattle.ReturnFire canReturnFire, final PlayerID firingPlayer, final PlayerID hitPlayer, final Collection<Unit> firingUnits, final String stepName, final String text, final MustFightBattle battle, final boolean defending, final Map<Unit, Collection<Unit>> dependentUnits, final ExecutionStack stack, final boolean headless, final Territory battleSite, final Collection<TerritoryEffect> territoryEffects, final List<Unit> allEnemyUnitsAliveOrWaitingToDie) { /* * This is to remove any Factories, AAguns, and Infrastructure from possible targets for the firing. * If, in the future, Infrastructure or other things could be taken casualty, then this will need to be changed back * to: * m_attackableUnits = attackableUnits; */ m_attackableUnits = Match.getMatches(attackableUnits, Matches.UnitIsNotInfrastructure); m_canReturnFire = canReturnFire; m_firingUnits = firingUnits; m_stepName = stepName; m_text = text; m_battle = battle; m_hitPlayer = hitPlayer; m_firingPlayer = firingPlayer; m_defending = defending; m_dependentUnits = dependentUnits; m_isHeadless = headless; m_battleID = battle.getBattleID(); m_battleSite = battleSite; m_territoryEffects = territoryEffects; m_allEnemyUnitsAliveOrWaitingToDie = allEnemyUnitsAliveOrWaitingToDie; m_allFriendlyUnitsNotIncludingWaitingToDie = m_defending ? m_battle.getDefendingUnits() : m_battle.getAttackingUnits(); m_allEnemyUnitsNotIncludingWaitingToDie = !m_defending ? m_battle.getDefendingUnits() : m_battle.getAttackingUnits(); m_isAmphibious = m_battle.isAmphibious(); m_amphibiousLandAttackers = m_battle.getAmphibiousLandAttackers(); } private void rollDice(final IDelegateBridge bridge) { if (m_dice != null) { throw new IllegalStateException("Already rolled"); } final List<Unit> units = new ArrayList<>(m_firingUnits); String annotation; if (m_isHeadless) { annotation = ""; } else { annotation = DiceRoll.getAnnotation(units, m_firingPlayer, m_battle); } m_dice = DiceRoll.rollDice(units, m_defending, m_firingPlayer, bridge, m_battle, annotation, m_territoryEffects, m_allEnemyUnitsAliveOrWaitingToDie); } private void selectCasualties(final IDelegateBridge bridge) { final int hitCount = m_dice.getHits(); AbstractBattle.getDisplay(bridge).notifyDice(m_dice, m_stepName); final int countTransports = Match.countMatches(m_attackableUnits, new CompositeMatchAnd<>(Matches.UnitIsTransport, Matches.UnitIsSea)); if (countTransports > 0 && isTransportCasualtiesRestricted(bridge.getData())) { CasualtyDetails message; final Collection<Unit> nonTransports = Match.getMatches(m_attackableUnits, new CompositeMatchOr<>(Matches.UnitIsNotTransportButCouldBeCombatTransport, Matches.UnitIsNotSea)); final Collection<Unit> transportsOnly = Match.getMatches(m_attackableUnits, new CompositeMatchAnd<>(Matches.UnitIsTransportButNotCombatTransport, Matches.UnitIsSea)); final int numPossibleHits = AbstractBattle.getMaxHits(nonTransports); // more hits than combat units if (hitCount > numPossibleHits) { int extraHits = hitCount - numPossibleHits; final Collection<PlayerID> alliedHitPlayer = new ArrayList<>(); // find the players who have transports in the attackable pile for (final Unit unit : transportsOnly) { if (!alliedHitPlayer.contains(unit.getOwner())) { alliedHitPlayer.add(unit.getOwner()); } } final Iterator<PlayerID> playerIter = alliedHitPlayer.iterator(); // Leave enough transports for each defender for overlfows so they can select who loses them. while (playerIter.hasNext()) { final PlayerID player = playerIter.next(); final CompositeMatch<Unit> match = new CompositeMatchAnd<>(); match.add(Matches.UnitIsTransportButNotCombatTransport); match.add(Matches.unitIsOwnedBy(player)); final Collection<Unit> playerTransports = Match.getMatches(transportsOnly, match); final int transportsToRemove = Math.max(0, playerTransports.size() - extraHits); transportsOnly.removeAll( Match.getNMatches(playerTransports, transportsToRemove, Matches.UnitIsTransportButNotCombatTransport)); } m_killed = nonTransports; m_damaged = Collections.emptyList(); // m_confirmOwnCasualties = true; if (extraHits > transportsOnly.size()) { extraHits = transportsOnly.size(); } message = BattleCalculator.selectCasualties(m_stepName, m_hitPlayer, transportsOnly, m_allEnemyUnitsNotIncludingWaitingToDie, m_firingPlayer, m_allFriendlyUnitsNotIncludingWaitingToDie, m_isAmphibious, m_amphibiousLandAttackers, m_battleSite, m_territoryEffects, bridge, m_text, m_dice, !m_defending, m_battleID, m_isHeadless, extraHits, true); m_killed.addAll(message.getKilled()); m_confirmOwnCasualties = true; } else if (hitCount == numPossibleHits) { // exact number of combat units m_killed = nonTransports; m_damaged = Collections.emptyList(); m_confirmOwnCasualties = true; } else { // less than possible number message = BattleCalculator.selectCasualties(m_stepName, m_hitPlayer, nonTransports, m_allEnemyUnitsNotIncludingWaitingToDie, m_firingPlayer, m_allFriendlyUnitsNotIncludingWaitingToDie, m_isAmphibious, m_amphibiousLandAttackers, m_battleSite, m_territoryEffects, bridge, m_text, m_dice, !m_defending, m_battleID, m_isHeadless, m_dice.getHits(), true); m_killed = message.getKilled(); m_damaged = message.getDamaged(); m_confirmOwnCasualties = message.getAutoCalculated(); } } else { // not isTransportCasualtiesRestricted // they all die if (hitCount >= AbstractBattle.getMaxHits(m_attackableUnits)) { m_killed = m_attackableUnits; m_damaged = Collections.emptyList(); // everything died, so we need to confirm m_confirmOwnCasualties = true; } else { // Choose casualties CasualtyDetails message; message = BattleCalculator.selectCasualties(m_stepName, m_hitPlayer, m_attackableUnits, m_allEnemyUnitsNotIncludingWaitingToDie, m_firingPlayer, m_allFriendlyUnitsNotIncludingWaitingToDie, m_isAmphibious, m_amphibiousLandAttackers, m_battleSite, m_territoryEffects, bridge, m_text, m_dice, !m_defending, m_battleID, m_isHeadless, m_dice.getHits(), true); m_killed = message.getKilled(); m_damaged = message.getDamaged(); m_confirmOwnCasualties = message.getAutoCalculated(); } } } private void notifyCasualties(final IDelegateBridge bridge) { if (m_isHeadless) { return; } AbstractBattle.getDisplay(bridge).casualtyNotification(m_battleID, m_stepName, m_dice, m_hitPlayer, new ArrayList<>(m_killed), new ArrayList<>(m_damaged), m_dependentUnits); final Runnable r = () -> { try { AbstractBattle.getRemote(m_firingPlayer, bridge).confirmEnemyCasualties(m_battleID, "Press space to continue", m_hitPlayer); } catch (final ConnectionLostException cle) { // somone else will deal with this // System.out.println(cle.getMessage()); // cle.printStackTrace(System.out); } catch (final Exception e) { // ignore } }; // execute in a seperate thread to allow either player to click continue first. final Thread t = new Thread(r, "Click to continue waiter"); t.start(); if (m_confirmOwnCasualties) { AbstractBattle.getRemote(m_hitPlayer, bridge).confirmOwnCasualties(m_battleID, "Press space to continue"); } try { bridge.leaveDelegateExecution(); t.join(); } catch (final InterruptedException e) { // ignore } finally { bridge.enterDelegateExecution(); } } /** * We must execute in atomic steps, push these steps onto the stack, and let them execute. */ @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { // add to the stack so we will execute, // we want to roll dice, select casualties, then notify in that order, so // push onto the stack in reverse order final IExecutable rollDice = new IExecutable() { private static final long serialVersionUID = 7578210876028725797L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { rollDice(bridge); } }; final IExecutable selectCasualties = new IExecutable() { private static final long serialVersionUID = -7687053541570519623L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { selectCasualties(bridge); } }; final IExecutable notifyCasualties = new IExecutable() { // compatible with 0.9.0.2 saved games private static final long serialVersionUID = -9173385989239225660L; @Override public void execute(final ExecutionStack stack, final IDelegateBridge bridge) { notifyCasualties(bridge); if (m_damaged != null) { m_battle.markDamaged(m_damaged, bridge); } m_battle.removeCasualties(m_killed, m_canReturnFire, !m_defending, bridge, false); } }; stack.push(notifyCasualties); stack.push(selectCasualties); stack.push(rollDice); } private boolean isTransportCasualtiesRestricted(final GameData data) { return games.strategy.triplea.Properties.getTransportCasualtiesRestricted(data); } }