package games.strategy.triplea.ui; import java.awt.Image; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; 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 javax.swing.JOptionPane; import games.strategy.debug.ClientLogger; import games.strategy.engine.data.GameData; import games.strategy.engine.data.PlayerID; import games.strategy.engine.data.Route; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.TechAttachment; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.delegate.AbstractMoveDelegate; import games.strategy.triplea.delegate.AbstractMoveDelegate.MoveType; import games.strategy.triplea.delegate.BaseEditDelegate; import games.strategy.triplea.delegate.GameStepPropertiesHelper; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.delegate.MoveValidator; import games.strategy.triplea.delegate.TransportTracker; import games.strategy.triplea.delegate.UnitComparator; import games.strategy.triplea.delegate.dataObjects.MoveDescription; import games.strategy.triplea.delegate.dataObjects.MoveValidationResult; import games.strategy.triplea.delegate.dataObjects.MustMoveWithDetails; import games.strategy.triplea.util.TransportUtils; import games.strategy.triplea.util.UnitCategory; 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; public class MovePanel extends AbstractMovePanel { private static final long serialVersionUID = 5004515340964828564L; private static final int s_defaultMinTransportCost = 5; /** * @param s_deselectNumber * adds or removes 10 units (used to remove 1/s_deselectNumber of total units (useful for splitting large * armies), but changed it * after feedback). */ private static final int s_deselectNumber = 10; // access only through getter and setter! private Territory firstSelectedTerritory; private Territory selectedEndpointTerritory; private Territory mouseCurrentTerritory; private Territory lastFocusedTerritory; private List<Territory> forced; private boolean nonCombat; private Point mouseSelectedPoint; private Point mouseCurrentPoint; private Point mouseLastUpdatePoint; // use a LinkedHashSet because we want to know the order private final Set<Unit> selectedUnits = new LinkedHashSet<>(); private static Map<Unit, Collection<Unit>> s_dependentUnits = new HashMap<>(); // the must move with details for the currently selected territory // note this is kept in sync because we do not modify selectedTerritory directly // instead we only do so through the private setter private MustMoveWithDetails mustMoveWithDetails = null; // cache this so we can update it only when territory/units change private List<Unit> unitsThatCanMoveOnRoute; private Image currentCursorImage; private Route routeCached = null; private String displayText = "Combat Move"; private MoveType moveType = MoveType.DEFAULT; /** Creates new MovePanel. */ public MovePanel(final GameData data, final MapPanel map, final TripleAFrame frame) { super(data, map, frame); m_undoableMovesPanel = new UndoableMovesPanel(data, this); mouseCurrentTerritory = null; unitsThatCanMoveOnRoute = Collections.emptyList(); currentCursorImage = null; } // Same as above! Delete this crap after refactoring. public static void clearDependents(final Collection<Unit> units) { for (final Unit unit : units) { if (Matches.UnitIsAirTransport.match(unit)) { s_dependentUnits.remove(unit); } } } @Override protected void clearDependencies() { s_dependentUnits.clear(); } public void setMoveType(final MoveType moveType) { this.moveType = moveType; } private PlayerID getUnitOwner(final Collection<Unit> units) { if (BaseEditDelegate.getEditMode(getData()) && units != null && !units.isEmpty()) { return units.iterator().next().getOwner(); } else { return getCurrentPlayer(); } } /** * Sort the specified units in preferred movement or unload order. */ private void sortUnitsToMove(final List<Unit> units, final Route route) { if (units == null || units.isEmpty()) { return; } else if (route == null) { final Exception nullRouteError = (new IllegalArgumentException("route is not supposed to be null")); ClientLogger.logQuietly( "Programming error, route should not be null here. Aborting sort operation and returning.", nullRouteError); } final Comparator<Unit> unitComparator; // sort units based on which transports are allowed to unload if (route.isUnload() && Match.someMatch(units, Matches.UnitIsLand)) { unitComparator = UnitComparator.getUnloadableUnitsComparator(units, route, getUnitOwner(units)); } else { unitComparator = UnitComparator.getMovableUnitsComparator(units, route); } Collections.sort(units, unitComparator); } /** * Sort the specified transports in preferred load order. */ private void sortTransportsToLoad(final List<Unit> transports, final Route route) { if (transports.isEmpty()) { return; } Collections.sort(transports, UnitComparator.getLoadableTransportsComparator(transports, route, getUnitOwner(transports))); } /** * Sort the specified transports in preferred unload order. */ private void sortTransportsToUnload(final List<Unit> transports, final Route route) { if (transports.isEmpty()) { return; } Collections.sort(transports, UnitComparator.getUnloadableTransportsComparator(transports, route, getUnitOwner(transports), true)); } /** * Return the units that are to be unloaded for this route. * If needed will ask the user what transports to unload. * This is needed because the user needs to be able to select what transports to unload * in the case where some transports have different movement, different * units etc */ private Collection<Unit> getUnitsToUnload(final Route route, final Collection<Unit> unitsToUnload) { final Collection<Unit> allUnits = getFirstSelectedTerritory().getUnits().getUnits(); final List<Unit> candidateUnits = Match.getMatches(allUnits, getUnloadableMatch(route, unitsToUnload)); if (unitsToUnload.size() == candidateUnits.size()) { return unitsToUnload; } final List<Unit> candidateTransports = Match.getMatches(allUnits, Matches.unitIsTransportingSomeCategories(candidateUnits)); // Remove all incapable transports final Collection<Unit> incapableTransports = Match.getMatches(candidateTransports, Matches.transportCannotUnload(route.getEnd())); candidateTransports.removeAll(incapableTransports); if (candidateTransports.size() == 0) { return Collections.emptyList(); } // Just one transport, don't bother to ask if (candidateTransports.size() == 1) { return unitsToUnload; } // Are the transports all of the same type and if they are, then don't ask final Collection<UnitCategory> categories = UnitSeperator.categorize(candidateTransports, mustMoveWithDetails.getMustMoveWith(), true, false); if (categories.size() == 1) { return unitsToUnload; } sortTransportsToUnload(candidateTransports, route); // unitsToUnload are actually dependents, but need to select transports final Set<Unit> defaultSelections = TransportUtils.findMinTransportsToUnload(unitsToUnload, candidateTransports); // Match criteria to ensure that chosen transports will match selected units final Match<Collection<Unit>> transportsToUnloadMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> units) { final List<Unit> sortedTransports = Match.getMatches(units, Matches.UnitIsTransport); final Collection<Unit> availableUnits = new ArrayList<>(unitsToUnload); // track the changing capacities of the transports as we assign units final IntegerMap<Unit> capacityMap = new IntegerMap<>(); for (final Unit transport : sortedTransports) { final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting(); capacityMap.add(transport, TransportUtils.getTransportCost(transporting)); } boolean hasChanged = false; final Comparator<Unit> increasingCapacityComparator = UnitComparator.getIncreasingCapacityComparator(sortedTransports); // This algorithm will ensure that it is actually possible to distribute // the selected units amongst the current selection of chosen transports. do { hasChanged = false; // Sort transports by increasing capacity Collections.sort(sortedTransports, increasingCapacityComparator); // Try to remove one unit from each transport, in succession final Iterator<Unit> transportIter = sortedTransports.iterator(); while (transportIter.hasNext()) { final Unit transport = transportIter.next(); final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting(); if (transporting == null) { continue; } final Collection<UnitCategory> transCategories = UnitSeperator.categorize(transporting); final Iterator<Unit> unitIter = availableUnits.iterator(); while (unitIter.hasNext()) { final Unit unit = unitIter.next(); final Collection<UnitCategory> unitCategory = UnitSeperator.categorize(Collections.singleton(unit)); // Is one of the transported units of the same type we want to unload? if (Util.someIntersect(transCategories, unitCategory)) { // Unload the unit, remove the transport from our list, and continue hasChanged = true; unitIter.remove(); transportIter.remove(); break; } } } // Repeat until there are no units left or no changes occur } while (availableUnits.size() > 0 && hasChanged); // If we haven't seen all of the transports (and removed them) then there are extra transports that don't fit return (sortedTransports.size() == 0); } }; // Choosing what transports to unload final UnitChooser chooser = new UnitChooser(candidateTransports, defaultSelections, mustMoveWithDetails.getMustMoveWith(), /* categorizeMovement */true, /* categorizeTransportCost */false, getGameData(), /* allowTwoHit */false, getMap().getUIContext(), transportsToUnloadMatch); chooser.setTitle("What transports do you want to unload"); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, "What transports do you want to unload", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return Collections.emptyList(); } final Collection<Unit> chosenTransports = Match.getMatches(chooser.getSelected(), Matches.UnitIsTransport); final List<Unit> allUnitsInSelectedTransports = new ArrayList<>(); for (final Unit transport : chosenTransports) { final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting(); if (transporting != null) { allUnitsInSelectedTransports.addAll(transporting); } } allUnitsInSelectedTransports.retainAll(candidateUnits); sortUnitsToMove(allUnitsInSelectedTransports, route); final List<Unit> rVal = new ArrayList<>(); final List<Unit> sortedTransports = new ArrayList<>(chosenTransports); Collections.sort(sortedTransports, UnitComparator.getIncreasingCapacityComparator(sortedTransports)); final Collection<Unit> selectedUnits = new ArrayList<>(unitsToUnload); // First pass: choose one unit from each selected transport for (final Unit transport : sortedTransports) { boolean hasChanged = false; final Iterator<Unit> selectedIter = selectedUnits.iterator(); while (selectedIter.hasNext()) { final Unit selected = selectedIter.next(); final Collection<Unit> transporting = TripleAUnit.get(transport).getTransporting(); for (final Unit candidate : transporting) { if (selected.getType().equals(candidate.getType()) && selected.getOwner().equals(candidate.getOwner()) && selected.getHits() == candidate.getHits()) { hasChanged = true; rVal.add(candidate); allUnitsInSelectedTransports.remove(candidate); selectedIter.remove(); break; } } if (hasChanged) { break; } } } // Now fill remaining slots in preferred unit order for (final Unit selected : selectedUnits) { final Iterator<Unit> candidateIter = allUnitsInSelectedTransports.iterator(); while (candidateIter.hasNext()) { final Unit candidate = candidateIter.next(); if (selected.getType().equals(candidate.getType()) && selected.getOwner().equals(candidate.getOwner()) && selected.getHits() == candidate.getHits()) { rVal.add(candidate); candidateIter.remove(); break; } } } return rVal; } private CompositeMatch<Unit> getUnloadableMatch(final Route route, final Collection<Unit> units) { final CompositeMatch<Unit> unloadable = new CompositeMatchAnd<>(); unloadable.add(getMovableMatch(route, units)); unloadable.add(Matches.UnitIsLand); return unloadable; } private CompositeMatch<Unit> getMovableMatch(final Route route, final Collection<Unit> units) { final CompositeMatch<Unit> movable = new CompositeMatchAnd<>(); if (!BaseEditDelegate.getEditMode(getData())) { movable.add(Matches.unitIsOwnedBy(getCurrentPlayer())); } /* * if you do not have selection of zero-movement units enabled, * this will restrict selection to units with 1 or more movement */ if (!games.strategy.triplea.Properties.getSelectableZeroMovementUnits(getData())) { movable.add(Matches.UnitCanMove); } if (!nonCombat) { movable.add(Matches.UnitCanNotMoveDuringCombatMove.invert()); } if (route != null) { final Match<Unit> enoughMovement = new Match<Unit>() { @Override public boolean match(final Unit u) { if (BaseEditDelegate.getEditMode(getData())) { return true; } return TripleAUnit.get(u).getMovementLeft() >= route.getMovementCost(u); } }; if (route.isUnload()) { final CompositeMatch<Unit> landOrCanMove = new CompositeMatchOr<>(); landOrCanMove.add(Matches.UnitIsLand); final CompositeMatch<Unit> notLandAndCanMove = new CompositeMatchAnd<>(); notLandAndCanMove.add(enoughMovement); notLandAndCanMove.add(Matches.UnitIsNotLand); landOrCanMove.add(notLandAndCanMove); movable.add(landOrCanMove); } else { movable.add(enoughMovement); } } if (route != null && route.getEnd() != null) { final boolean water = route.getEnd().isWater(); if (water && !route.isLoad()) { movable.add(Matches.UnitIsNotLand); } if (!water) { movable.add(Matches.UnitIsNotSea); } } if (units != null && !units.isEmpty()) { // force all units to have the same owner in edit mode final PlayerID owner = getUnitOwner(units); if (BaseEditDelegate.getEditMode(getData())) { movable.add(Matches.unitIsOwnedBy(owner)); } final CompositeMatch<Unit> rightUnitTypeMatch = new CompositeMatchOr<>(); for (final Unit unit : units) { if (unit.getOwner().equals(owner)) { rightUnitTypeMatch.add(Matches.unitIsOfType(unit.getType())); } } movable.add(rightUnitTypeMatch); } return movable; } private Route getRoute(final Territory start, final Territory end, final Collection<Unit> selectedUnits) { getData().acquireReadLock(); try { if (forced == null) { return getRouteNonForced(start, end, selectedUnits); } else { return getRouteForced(start, end, selectedUnits); } } finally { getData().releaseReadLock(); } } /** * Get the route including the territories that we are forced to move through. */ private Route getRouteForced(final Territory start, final Territory end, final Collection<Unit> selectedUnits) { if (forced == null || forced.size() == 0) { throw new IllegalStateException("No forced territories:" + forced + " end:" + end + " start:" + start); } final Iterator<Territory> iter = forced.iterator(); Territory last = getFirstSelectedTerritory(); Territory current = null; Route total = new Route(); total.setStart(last); while (iter.hasNext()) { current = iter.next(); final Route add = getData().getMap().getRoute(last, current); final Route newTotal = Route.join(total, add); if (newTotal == null) { return total; } total = newTotal; last = current; } if (!end.equals(last)) { final Route add = getRouteNonForced(last, end, selectedUnits); final Route newTotal = Route.join(total, add); if (newTotal != null) { total = newTotal; } } return total; } /** * Get the route ignoring forced territories. */ private Route getRouteNonForced(final Territory start, final Territory end, final Collection<Unit> selectedUnits) { // can't rely on current player being the unit owner in Edit Mode // look at the units being moved to determine allies and enemies final PlayerID owner = getUnitOwner(selectedUnits); return MoveValidator.getBestRoute(start, end, getData(), owner, selectedUnits, !GameStepPropertiesHelper.isAirborneMove(getData())); } private void updateUnitsThatCanMoveOnRoute(final Collection<Unit> units, final Route route) { if (route == null || route.hasNoSteps()) { clearStatusMessage(); getMap().showMouseCursor(); currentCursorImage = null; unitsThatCanMoveOnRoute = new ArrayList<>(units); return; } getMap().hideMouseCursor(); // TODO kev check for already loaded airTransports Collection<Unit> transportsToLoad = Collections.emptyList(); if (MoveValidator.isLoad(units, s_dependentUnits, route, getData(), getCurrentPlayer())) { transportsToLoad = route.getEnd().getUnits().getMatches( new CompositeMatchAnd<>(Matches.UnitIsTransport, Matches.alliedUnit(getCurrentPlayer(), getData()))); } List<Unit> best = new ArrayList<>(units); // if the player selects a land unit and other units // when the // only consider the non land units if (route.getStart().isWater() && route.getEnd() != null && route.getEnd().isWater() && !route.isLoad()) { best = Match.getMatches(best, new InverseMatch<>(Matches.UnitIsLand)); } sortUnitsToMove(best, route); Collections.reverse(best); List<Unit> bestWithDependents = addMustMoveWith(best); MoveValidationResult allResults; getData().acquireReadLock(); try { allResults = AbstractMoveDelegate.validateMove(moveType, bestWithDependents, route, getCurrentPlayer(), transportsToLoad, s_dependentUnits, nonCombat, getUndoableMoves(), getData()); } finally { getData().releaseReadLock(); } MoveValidationResult lastResults = allResults; if (!allResults.isMoveValid()) { // if the player is invading only consider units that can invade if (!nonCombat && route.isUnload() && Matches.isTerritoryEnemy(getCurrentPlayer(), getData()).match(route.getEnd())) { best = Match.getMatches(best, Matches.UnitCanInvade); bestWithDependents = addMustMoveWith(best); lastResults = AbstractMoveDelegate.validateMove(moveType, bestWithDependents, route, getCurrentPlayer(), transportsToLoad, s_dependentUnits, nonCombat, getUndoableMoves(), getData()); } while (!best.isEmpty() && !lastResults.isMoveValid()) { best = best.subList(1, best.size()); bestWithDependents = addMustMoveWith(best); lastResults = AbstractMoveDelegate.validateMove(moveType, bestWithDependents, route, getCurrentPlayer(), transportsToLoad, s_dependentUnits, nonCombat, getUndoableMoves(), getData()); } } if (allResults.isMoveValid()) { // valid move if (bestWithDependents.containsAll(selectedUnits)) { clearStatusMessage(); currentCursorImage = null; } else { setStatusWarningMessage("Not all units can move there"); currentCursorImage = getMap().getWarningImage().orElse(null); } } else { String message = allResults.getError(); if (message == null) { message = allResults.getDisallowedUnitWarning(0); } if (message == null) { message = allResults.getUnresolvedUnitWarning(0); } if (!lastResults.isMoveValid()) { setStatusErrorMessage(message); currentCursorImage = getMap().getErrorImage().orElse(null); } else { setStatusWarningMessage(message); currentCursorImage = getMap().getWarningImage().orElse(null); } } if (unitsThatCanMoveOnRoute.size() != new HashSet<>(unitsThatCanMoveOnRoute).size()) { cancelMove(); return; } unitsThatCanMoveOnRoute = new ArrayList<>(bestWithDependents); } private List<Unit> addMustMoveWith(final List<Unit> best) { final List<Unit> bestWithDependents = new ArrayList<>(best); for (final Unit u : best) { if (mustMoveWithDetails.getMustMoveWith().containsKey(u)) { final Collection<Unit> mustMoveWith = mustMoveWithDetails.getMustMoveWith().get(u); if (mustMoveWith != null) { for (final Unit m : mustMoveWith) { if (!bestWithDependents.contains(m)) { bestWithDependents.addAll(mustMoveWith); } } } } } return bestWithDependents; } /** * Route can be null. */ final void updateRouteAndMouseShadowUnits(final Route route) { routeCached = route; getMap().setRoute(route, mouseSelectedPoint, mouseCurrentPoint, currentCursorImage); if (route == null) { getMap().setMouseShadowUnits(null); } else { getMap().setMouseShadowUnits(unitsThatCanMoveOnRoute); } } /** * Allow the user to select what transports to load. * If null is returned, the move should be canceled. */ private Collection<Unit> getTransportsToLoad(final Route route, final Collection<Unit> unitsToLoad, final boolean disablePrompts) { if (!route.isLoad()) { return Collections.emptyList(); } if (Match.someMatch(unitsToLoad, Matches.UnitIsAir)) { return Collections.emptyList(); } final Collection<Unit> endOwnedUnits = route.getEnd().getUnits().getUnits(); final PlayerID unitOwner = getUnitOwner(unitsToLoad); final MustMoveWithDetails endMustMoveWith = MoveValidator.getMustMoveWith(route.getEnd(), endOwnedUnits, s_dependentUnits, getData(), unitOwner); int minTransportCost = s_defaultMinTransportCost; for (final Unit unit : unitsToLoad) { minTransportCost = Math.min(minTransportCost, UnitAttachment.get(unit.getType()).getTransportCost()); } final CompositeMatch<Unit> candidateTransportsMatch = new CompositeMatchAnd<>(); candidateTransportsMatch.add(Matches.UnitIsTransport); candidateTransportsMatch.add(Matches.alliedUnit(unitOwner, getGameData())); final List<Unit> candidateTransports = Match.getMatches(endOwnedUnits, candidateTransportsMatch); // remove transports that don't have enough capacity final Iterator<Unit> transportIter = candidateTransports.iterator(); while (transportIter.hasNext()) { final Unit transport = transportIter.next(); final int capacity = TransportTracker.getAvailableCapacity(transport); if (capacity < minTransportCost) { transportIter.remove(); } } // nothing to choose if (candidateTransports.isEmpty()) { return Collections.emptyList(); } // sort transports in preferred load order sortTransportsToLoad(candidateTransports, route); final List<Unit> availableUnits = new ArrayList<>(unitsToLoad); final IntegerMap<Unit> availableCapacityMap = new IntegerMap<>(); for (final Unit transport : candidateTransports) { final int capacity = TransportTracker.getAvailableCapacity(transport); availableCapacityMap.put(transport, capacity); } final Set<Unit> defaultSelections = new HashSet<>(); // Algorithm to choose defaultSelections (transports to load) // We are trying to determine which transports are the best defaults to select for loading, // and so we need a modified algorithm based strictly on candidateTransports order: // - owned, capable transports are chosen first; attempt to fill them // - allied, capable transports are chosen next; attempt to fill them // - finally, incapable transports are chosen last (will generate errors) // Note that if any allied transports qualify as defaults, we will always prompt with a // UnitChooser later on so that it is obvious to the player. boolean useAlliedTransports = false; final Collection<Unit> capableTransports = new ArrayList<>(candidateTransports); // only allow incapable transports for updateUnitsThatCanMoveOnRoute // so that we can have a nice UI error shown if these transports // are selected, since it may not be obvious final Collection<Unit> incapableTransports = Match.getMatches(capableTransports, Matches.transportCannotUnload(route.getEnd())); capableTransports.removeAll(incapableTransports); final Match<Unit> alliedMatch = new Match<Unit>() { @Override public boolean match(final Unit transport) { return (!transport.getOwner().equals(unitOwner)); } }; final Collection<Unit> alliedTransports = Match.getMatches(capableTransports, alliedMatch); capableTransports.removeAll(alliedTransports); // First, load capable transports final Map<Unit, Unit> unitsToCapableTransports = TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, capableTransports); for (final Unit unit : unitsToCapableTransports.keySet()) { final Unit transport = unitsToCapableTransports.get(unit); final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost(); availableCapacityMap.add(transport, (-1 * unitCost)); defaultSelections.add(transport); } availableUnits.removeAll(unitsToCapableTransports.keySet()); // Next, load allied transports final Map<Unit, Unit> unitsToAlliedTransports = TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, alliedTransports); for (final Unit unit : unitsToAlliedTransports.keySet()) { final Unit transport = unitsToAlliedTransports.get(unit); final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost(); availableCapacityMap.add(transport, (-1 * unitCost)); defaultSelections.add(transport); useAlliedTransports = true; } availableUnits.removeAll(unitsToAlliedTransports.keySet()); // only allow incapable transports for updateUnitsThatCanMoveOnRoute // so that we can have a nice UI error shown if these transports // are selected, since it may not be obvious if (getSelectedEndpointTerritory() == null) { final Map<Unit, Unit> unitsToIncapableTransports = TransportUtils.mapTransportsToLoadUsingMinTransports(availableUnits, incapableTransports); for (final Unit unit : unitsToIncapableTransports.keySet()) { final Unit transport = unitsToIncapableTransports.get(unit); final int unitCost = UnitAttachment.get(unit.getType()).getTransportCost(); availableCapacityMap.add(transport, (-1 * unitCost)); defaultSelections.add(transport); } availableUnits.removeAll(unitsToIncapableTransports.keySet()); } else { candidateTransports.removeAll(incapableTransports); } // return defaults if we aren't allowed to prompt if (disablePrompts) { return defaultSelections; } // force UnitChooser to pop up if we are choosing allied transports if (!useAlliedTransports) { if (candidateTransports.size() == 1) { return candidateTransports; } // all the same type, dont ask unless we have more than 1 unit type if (UnitSeperator.categorize(candidateTransports, endMustMoveWith.getMustMoveWith(), true, false).size() == 1 && unitsToLoad.size() == 1) { return candidateTransports; } // If we've filled all transports, then no user intervention is required. // It is possible to make "wrong" decisions if there are mixed unit types and // mixed transport categories, but there is no UI to manage that anyway. // Players will need to load incrementally in such cases. if (defaultSelections.containsAll(candidateTransports)) { return candidateTransports; } } // the match criteria to ensure that chosen transports will match selected units final Match<Collection<Unit>> transportsToLoadMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> units) { final Collection<Unit> transports = Match.getMatches(units, Matches.UnitIsTransport); // prevent too many transports from being selected return (transports.size() <= Math.min(unitsToLoad.size(), candidateTransports.size())); } }; final UnitChooser chooser = new UnitChooser(candidateTransports, defaultSelections, endMustMoveWith.getMustMoveWith(), /* categorizeMovement */true, /* categorizeTransportCost */false, getGameData(), /* allowTwoHit */false, getMap().getUIContext(), transportsToLoadMatch); chooser.setTitle("What transports do you want to load"); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, "What transports do you want to load", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return Collections.emptyList(); } return chooser.getSelected(false); } private final UnitSelectionListener UNIT_SELECTION_LISTENER = new UnitSelectionListener() { @Override public void unitsSelected(final List<Unit> units, final Territory t, final MouseDetails me) { if (!getListening()) { return; } // check if we can handle this event, are we active? if (!getActive()) { return; } if (t == null) { return; } final boolean rightMouse = me.isRightButton(); final boolean isMiddleMouseButton = me.getButton() == MouseEvent.BUTTON2; final boolean noSelectedTerritory = (firstSelectedTerritory == null); final boolean isFirstSelectedTerritory = (firstSelectedTerritory == t); // select units final GameData data = getData(); data.acquireReadLock(); try { // de select units if (rightMouse && !noSelectedTerritory && !m_map.wasLastActionDraggingAndReset()) { deselectUnits(units, t, me); } else if (!isMiddleMouseButton && !rightMouse && (noSelectedTerritory || isFirstSelectedTerritory)) { selectUnitsToMove(units, t, me); } else if (!rightMouse && me.isControlDown() && !isFirstSelectedTerritory) { selectWayPoint(t); } else if (!rightMouse && !noSelectedTerritory && !isFirstSelectedTerritory && !isMiddleMouseButton) { selectEndPoint(t); } } finally { data.releaseReadLock(); } getMap().requestFocusInWindow(); } private void selectUnitsToMove(final List<Unit> units, final Territory t, final MouseDetails me) { // are any of the units ours, note - if no units selected thats still ok if (!BaseEditDelegate.getEditMode(getData()) || !selectedUnits.isEmpty()) { for (final Unit unit : units) { if (!unit.getOwner().equals(getUnitOwner(selectedUnits))) { return; } } } // basic match criteria only final CompositeMatch<Unit> unitsToMoveMatch = getMovableMatch(null, null); final Match<Collection<Unit>> ownerMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> unitsToCheck) { final PlayerID owner = unitsToCheck.iterator().next().getOwner(); for (final Unit unit : unitsToCheck) { if (!owner.equals(unit.getOwner())) { return false; } } return true; } }; if (units.isEmpty() && selectedUnits.isEmpty()) { if (!me.isShiftDown()) { final List<Unit> unitsToMove = t.getUnits().getMatches(unitsToMoveMatch); if (unitsToMove.isEmpty()) { return; } final String text = "Select units to move from " + t.getName(); UnitChooser chooser; if (BaseEditDelegate.getEditMode(getData()) && !Match .getMatches(unitsToMove, Matches.unitIsOwnedBy(getUnitOwner(unitsToMove))).containsAll(unitsToMove)) { // use matcher to prevent units of different owners being chosen chooser = new UnitChooser(unitsToMove, selectedUnits, /* mustMoveWith */null, /* categorizeMovement */false, /* categorizeTransportCost */false, getData(), /* allowTwoHit */false, getMap().getUIContext(), ownerMatch); } else { chooser = new UnitChooser(unitsToMove, selectedUnits, /* mustMoveWith */null, /* categorizeMovement */false, /* categorizeTransportCost */false, getData(), /* allowTwoHit */false, getMap().getUIContext()); } final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, text, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return; } if (chooser.getSelected(false).isEmpty()) { return; } selectedUnits.addAll(chooser.getSelected(false)); } } if (getFirstSelectedTerritory() == null) { setFirstSelectedTerritory(t); mouseSelectedPoint = me.getMapPoint(); mouseCurrentPoint = me.getMapPoint(); enableCancelButton(); } if (!getFirstSelectedTerritory().equals(t)) { throw new IllegalStateException("Wrong selected territory"); } // add all if (me.isShiftDown()) { // prevent units of multiple owners from being chosen in edit mode final CompositeMatch<Unit> ownedNotFactory = new CompositeMatchAnd<>(); if (!BaseEditDelegate.getEditMode(getData())) { ownedNotFactory.add(unitsToMoveMatch); } else if (!selectedUnits.isEmpty()) { ownedNotFactory.add(unitsToMoveMatch); ownedNotFactory.add(Matches.unitIsOwnedBy(getUnitOwner(selectedUnits))); } else { ownedNotFactory.add(unitsToMoveMatch); ownedNotFactory.add(Matches.unitIsOwnedBy(getUnitOwner(t.getUnits().getUnits()))); } selectedUnits.addAll(t.getUnits().getMatches(ownedNotFactory)); } else if (me.isControlDown()) { selectedUnits.addAll(Match.getMatches(units, unitsToMoveMatch)); } else { // add one // best candidate unit for route is chosen dynamically later // check for alt key - add 1/10 of total units (useful for splitting large armies) final List<Unit> unitsToMove = Match.getMatches(units, unitsToMoveMatch); Collections.sort(unitsToMove, UnitComparator.getHighestToLowestMovementComparator()); final int iterCount = (me.isAltDown()) ? s_deselectNumber : 1; int addCount = 0; for (final Unit unit : unitsToMove) { if (!selectedUnits.contains(unit)) { selectedUnits.add(unit); addCount++; if (addCount >= iterCount) { break; } } } } if (!selectedUnits.isEmpty()) { mouseLastUpdatePoint = me.getMapPoint(); final Route route = getRoute(getFirstSelectedTerritory(), t, selectedUnits); // Load Bombers with paratroops if ((!nonCombat || IsParatroopersCanMoveDuringNonCombat(getData())) && TechAttachment.isAirTransportable(getCurrentPlayer()) && Match.someMatch(selectedUnits, new CompositeMatchAnd<>(Matches.UnitIsAirTransport, Matches.unitHasNotMoved))) { final PlayerID player = getCurrentPlayer(); // TODO Transporting allied units // Get the potential units to load final CompositeMatch<Unit> unitsToLoadMatch = new CompositeMatchAnd<>(); unitsToLoadMatch.add(Matches.UnitIsAirTransportable); unitsToLoadMatch.add(Matches.unitIsOwnedBy(player)); unitsToLoadMatch.add(Matches.unitHasNotMoved); final Collection<Unit> unitsToLoad = Match.getMatches(route.getStart().getUnits().getUnits(), unitsToLoadMatch); unitsToLoad.removeAll(selectedUnits); for (final Unit u : s_dependentUnits.keySet()) { unitsToLoad.removeAll(s_dependentUnits.get(u)); } // Get the potential air transports to load final CompositeMatch<Unit> candidateAirTransportsMatch = new CompositeMatchAnd<>(); candidateAirTransportsMatch.add(Matches.UnitIsAirTransport); candidateAirTransportsMatch.add(Matches.unitIsOwnedBy(player)); candidateAirTransportsMatch.add(Matches.unitHasNotMoved); candidateAirTransportsMatch.add(Matches.transportIsNotTransporting()); final Collection<Unit> candidateAirTransports = Match.getMatches(t.getUnits().getMatches(unitsToMoveMatch), candidateAirTransportsMatch); // candidateAirTransports.removeAll(selectedUnits); candidateAirTransports.removeAll(s_dependentUnits.keySet()); if (unitsToLoad.size() > 0 && candidateAirTransports.size() > 0) { final Collection<Unit> airTransportsToLoad = getAirTransportsToLoad(candidateAirTransports); selectedUnits.addAll(airTransportsToLoad); if (!airTransportsToLoad.isEmpty()) { final Collection<Unit> loadedAirTransports = getLoadedAirTransports(route, unitsToLoad, airTransportsToLoad, player); selectedUnits.addAll(loadedAirTransports); final MoveDescription message = new MoveDescription(loadedAirTransports, route, airTransportsToLoad, s_dependentUnits); setMoveMessage(message); } } } updateUnitsThatCanMoveOnRoute(selectedUnits, route); updateRouteAndMouseShadowUnits(route); } else { setFirstSelectedTerritory(null); } } public Collection<Unit> getAirTransportsToLoad(final Collection<Unit> candidateAirTransports) { final Set<Unit> defaultSelections = new HashSet<>(); // prevent too many bombers from being selected final Match<Collection<Unit>> transportsToLoadMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> units) { final Collection<Unit> airTransports = Match.getMatches(units, Matches.UnitIsAirTransport); return (airTransports.size() <= candidateAirTransports.size()); } }; // Allow player to select which to load. final UnitChooser chooser = new UnitChooser(candidateAirTransports, defaultSelections, s_dependentUnits, /* categorizeMovement */true, /* categorizeTransportCost */false, getGameData(), /* allowTwoHit */false, getMap().getUIContext(), transportsToLoadMatch); chooser.setTitle("Select air transports to load"); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, "What transports do you want to load", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return Collections.emptyList(); } return chooser.getSelected(true); } /** * Allow the user to select what units to load. * If null is returned, the move should be canceled. */ public Collection<Unit> getLoadedAirTransports(final Route route, final Collection<Unit> capableUnitsToLoad, final Collection<Unit> capableTransportsToLoad, final PlayerID player) { // Get the minimum transport cost of a candidate unit int minTransportCost = Integer.MAX_VALUE; for (final Unit unit : capableUnitsToLoad) { minTransportCost = Math.min(minTransportCost, UnitAttachment.get(unit.getType()).getTransportCost()); } final Collection<Unit> airTransportsToLoad = new ArrayList<>(); for (final Unit bomber : capableTransportsToLoad) { final int capacity = TransportTracker.getAvailableCapacity(bomber); if (capacity >= minTransportCost) { airTransportsToLoad.add(bomber); } } // If no airTransports can be loaded, return the empty set if (airTransportsToLoad.isEmpty()) { return airTransportsToLoad; } final Set<Unit> defaultSelections = new HashSet<>(); // Check to see if there's room for the selected units final Match<Collection<Unit>> unitsToLoadMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> units) { final Collection<Unit> unitsToLoad = Match.getMatches(units, Matches.UnitIsAirTransportable); final Map<Unit, Unit> unitMap = TransportUtils.mapTransportsToLoad(unitsToLoad, airTransportsToLoad); boolean ableToLoad = true; for (final Unit unit : unitsToLoad) { if (!unitMap.keySet().contains(unit)) { ableToLoad = false; } } return ableToLoad; } }; List<Unit> loadedUnits = new ArrayList<>(capableUnitsToLoad); if (!airTransportsToLoad.isEmpty()) { // Get a list of the units that could be loaded on the transport (based upon transport capacity) final List<Unit> unitsToLoad = TransportUtils.findUnitsToLoadOnAirTransports(capableUnitsToLoad, airTransportsToLoad); final String title = "Load air transports"; final String action = "load"; loadedUnits = UserChooseUnits(defaultSelections, unitsToLoadMatch, unitsToLoad, title, action); final Map<Unit, Unit> mapping = TransportUtils.mapTransportsToLoad(loadedUnits, airTransportsToLoad); for (final Unit unit : mapping.keySet()) { final Collection<Unit> unitsColl = new ArrayList<>(); unitsColl.add(unit); final Unit airTransport = mapping.get(unit); if (s_dependentUnits.containsKey(airTransport)) { unitsColl.addAll(s_dependentUnits.get(airTransport)); } s_dependentUnits.put(airTransport, unitsColl); mustMoveWithDetails = MoveValidator.getMustMoveWith(route.getStart(), route.getStart().getUnits().getUnits(), s_dependentUnits, getData(), player); } } return loadedUnits; } private void deselectUnits(List<Unit> units, final Territory t, final MouseDetails me) { final Collection<Unit> unitsToRemove = new ArrayList<>(selectedUnits.size()); // we have right clicked on a unit stack in a different territory if (!getFirstSelectedTerritory().equals(t)) { units = Collections.emptyList(); } // remove the dependent units so we don't have to micromanage them final List<Unit> unitsWithoutDependents = new ArrayList<>(selectedUnits); for (final Unit unit : selectedUnits) { final Collection<Unit> forced = mustMoveWithDetails.getMustMoveWith().get(unit); if (forced != null) { unitsWithoutDependents.removeAll(forced); } } // no unit selected, remove the most recent, but skip dependents if (units.isEmpty()) { if (me.isControlDown()) { selectedUnits.clear(); // Clear the stored dependents for AirTransports if (!s_dependentUnits.isEmpty()) { s_dependentUnits.clear(); } } else if (!unitsWithoutDependents.isEmpty()) { // check for alt key - remove 1/10 of total units (useful for splitting large armies) final int iterCount = (me.isAltDown()) ? s_deselectNumber : 1; // remove the last iterCount elements for (int i = 0; i < iterCount; i++) { unitsToRemove.add(unitsWithoutDependents.get(unitsWithoutDependents.size() - 1)); // Clear the stored dependents for AirTransports if (!s_dependentUnits.isEmpty()) { for (final Unit airTransport : unitsWithoutDependents) { if (s_dependentUnits.containsKey(airTransport)) { unitsToRemove.addAll(s_dependentUnits.get(airTransport)); s_dependentUnits.remove(airTransport); } } } } } } else { // we have actually clicked on a specific unit // remove all if control is down if (me.isControlDown()) { unitsToRemove.addAll(units); // Clear the stored dependents for AirTransports if (!s_dependentUnits.isEmpty()) { for (final Unit airTransport : unitsWithoutDependents) { if (s_dependentUnits.containsKey(airTransport)) { unitsToRemove.addAll(s_dependentUnits.get(airTransport)); s_dependentUnits.remove(airTransport); } } } } else { // remove one if (!getFirstSelectedTerritory().equals(t)) { throw new IllegalStateException("Wrong selected territory"); } // doesn't matter which unit we remove since units are assigned to routes later // check for alt key - remove 1/10 of total units (useful for splitting large armies) // changed to just remove 10 units // (int) Math.max(1, Math.floor(units.size() / s_deselectNumber)) final int iterCount = (me.isAltDown()) ? s_deselectNumber : 1; int remCount = 0; for (final Unit unit : units) { if (selectedUnits.contains(unit) && !unitsToRemove.contains(unit)) { unitsToRemove.add(unit); // Clear the stored dependents for AirTransports if (!s_dependentUnits.isEmpty()) { for (final Unit airTransport : unitsWithoutDependents) { if (s_dependentUnits.containsKey(airTransport)) { s_dependentUnits.get(airTransport).remove(unit); } } } remCount++; if (remCount >= iterCount) { break; } } } } } // perform the remove selectedUnits.removeAll(unitsToRemove); if (selectedUnits.isEmpty()) { // nothing left, cancel move cancelMove(); } else { mouseLastUpdatePoint = me.getMapPoint(); updateUnitsThatCanMoveOnRoute(selectedUnits, getRoute(getFirstSelectedTerritory(), t, selectedUnits)); updateRouteAndMouseShadowUnits(getRoute(getFirstSelectedTerritory(), t, selectedUnits)); } } private void selectWayPoint(final Territory territory) { if (forced == null) { forced = new ArrayList<>(); } if (!forced.contains(territory)) { forced.add(territory); } updateRouteAndMouseShadowUnits( getRoute(getFirstSelectedTerritory(), getFirstSelectedTerritory(), selectedUnits)); } private CompositeMatch<Unit> getUnloadableMatch() { // are we unloading everything? if we are then we dont need to select the transports final CompositeMatch<Unit> unloadable = new CompositeMatchAnd<>(); unloadable.add(Matches.unitIsOwnedBy(getCurrentPlayer())); unloadable.add(Matches.UnitIsLand); if (nonCombat) { unloadable.add(Matches.UnitCanNotMoveDuringCombatMove.invert()); } return unloadable; } private void selectEndPoint(final Territory territory) { final Route route = getRoute(getFirstSelectedTerritory(), territory, selectedUnits); final List<Unit> units = unitsThatCanMoveOnRoute; setSelectedEndpointTerritory(territory); if (units.isEmpty() || route == null) { cancelMove(); return; } Collection<Unit> transports = null; final CompositeMatch<Unit> paratroopNBombers = new CompositeMatchAnd<>(); paratroopNBombers.add(Matches.UnitIsAirTransport); paratroopNBombers.add(Matches.UnitIsAirTransportable); final boolean paratroopsLanding = Match.someMatch(units, paratroopNBombers); if (route.isLoad() && Match.someMatch(units, Matches.UnitIsLand)) { transports = getTransportsToLoad(route, units, false); if (transports.isEmpty()) { cancelMove(); return; } } else if ((route.isUnload() && Match.someMatch(units, Matches.UnitIsLand)) || paratroopsLanding) { final List<Unit> unloadAble = Match.getMatches(selectedUnits, getUnloadableMatch()); final Collection<Unit> canMove = new ArrayList<>(getUnitsToUnload(route, unloadAble)); canMove.addAll(Match.getMatches(selectedUnits, new InverseMatch<>(getUnloadableMatch()))); if (paratroopsLanding) { transports = canMove; } if (canMove.isEmpty()) { cancelMove(); return; } else { selectedUnits.clear(); selectedUnits.addAll(canMove); } } else { // keep a map of the max number of each eligible unitType that can be chosen final IntegerMap<UnitType> maxMap = new IntegerMap<>(); for (final Unit unit : units) { maxMap.add(unit.getType(), 1); } // this match will make sure we can't select more units // of a specific type then we had originally selected final Match<Collection<Unit>> unitTypeCountMatch = new Match<Collection<Unit>>() { @Override public boolean match(final Collection<Unit> units) { final IntegerMap<UnitType> currentMap = new IntegerMap<>(); for (final Unit unit : units) { currentMap.add(unit.getType(), 1); } return maxMap.greaterThanOrEqualTo(currentMap); } }; allowSpecificUnitSelection(units, route, false, unitTypeCountMatch); if (units.isEmpty()) { cancelMove(); return; } } final MoveDescription message = new MoveDescription(units, route, transports, s_dependentUnits); setMoveMessage(message); setFirstSelectedTerritory(null); setSelectedEndpointTerritory(null); mouseCurrentTerritory = null; forced = null; updateRouteAndMouseShadowUnits(null); release(); } }; /** * Allow the user to select specific units, if for example some units * have different movement * Units are sorted in preferred order, so units represents the default selections. */ private boolean allowSpecificUnitSelection(final Collection<Unit> units, final Route route, boolean mustQueryUser, final Match<Collection<Unit>> matchCriteria) { final List<Unit> candidateUnits = getFirstSelectedTerritory().getUnits().getMatches(getMovableMatch(route, units)); if (!mustQueryUser) { final Set<UnitCategory> categories = UnitSeperator.categorize(candidateUnits, mustMoveWithDetails.getMustMoveWith(), true, false); for (final UnitCategory category1 : categories) { // we cant move these, dont bother to check if (category1.getMovement() == 0) { continue; } for (final UnitCategory category2 : categories) { // we cant move these, dont bother to check if (category2.getMovement() == 0) { continue; } // if we find that two categories are compatable, and some units // are selected from one category, but not the other // then the user has to refine his selection if (category1 != category2 && category1.getType() == category2.getType() && !category1.equals(category2)) { // if we are moving all the units from both categories, then nothing to choose if (units.containsAll(category1.getUnits()) && units.containsAll(category2.getUnits())) { continue; } // if we are moving some of the units from either category, then we need to stop if (!Util.intersection(category1.getUnits(), units).isEmpty() || !Util.intersection(category2.getUnits(), units).isEmpty()) { mustQueryUser = true; } } } } } if (mustQueryUser) { final List<Unit> defaultSelections = new ArrayList<>(units.size()); if (route.isLoad()) { final Collection<Unit> transportsToLoad = new ArrayList<>(getTransportsToLoad(route, units, false)); defaultSelections.addAll(TransportUtils.mapTransports(route, units, transportsToLoad).keySet()); } else { defaultSelections.addAll(units); } // sort candidateUnits in preferred order sortUnitsToMove(candidateUnits, route); final UnitChooser chooser = new UnitChooser(candidateUnits, defaultSelections, mustMoveWithDetails.getMustMoveWith(), true, false, getGameData(), false, getMap().getUIContext(), matchCriteria); final String text = "Select units to move from " + getFirstSelectedTerritory() + "."; final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, text, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { units.clear(); return false; } units.clear(); units.addAll(chooser.getSelected(false)); } // add the dependent units final List<Unit> unitsCopy = new ArrayList<>(units); for (final Unit unit : unitsCopy) { final Collection<Unit> forced = mustMoveWithDetails.getMustMoveWith().get(unit); if (forced != null) { // add dependent if necessary for (final Unit dependent : forced) { if (unitsCopy.indexOf(dependent) == -1) { units.add(dependent); } } } } return true; } private final MouseOverUnitListener MOUSE_OVER_UNIT_LISTENER = new MouseOverUnitListener() { @Override public void mouseEnter(final List<Unit> units, final Territory territory, final MouseDetails me) { if (!getListening()) { return; } final PlayerID owner = getUnitOwner(selectedUnits); final CompositeMatchAnd<Unit> match = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(owner)/* , Matches.UnitIsNotFactory */); match.add(Matches.UnitCanMove); final boolean someOwned = Match.someMatch(units, match); final boolean isCorrectTerritory = firstSelectedTerritory == null || firstSelectedTerritory == territory; if (someOwned && isCorrectTerritory) { final Map<Territory, List<Unit>> highlight = new HashMap<>(); highlight.put(territory, units); getMap().setUnitHighlight(highlight); } else { getMap().setUnitHighlight(null); } } }; private final MapSelectionListener MAP_SELECTION_LISTENER = new DefaultMapSelectionListener() { @Override public void territorySelected(final Territory territory, final MouseDetails me) {} @Override public void mouseMoved(final Territory territory, final MouseDetails me) { if (!getListening()) { return; } if (getFirstSelectedTerritory() != null && territory != null) { Route route; if (mouseCurrentTerritory == null || !mouseCurrentTerritory.equals(territory) || mouseCurrentPoint.equals(mouseLastUpdatePoint)) { route = getRoute(getFirstSelectedTerritory(), territory, selectedUnits); getData().acquireReadLock(); try { updateUnitsThatCanMoveOnRoute(selectedUnits, route); // now, check if there is a better route for just the units that can get there (we check only air since that // is the only one for // which the route may actually change much) if (unitsThatCanMoveOnRoute.size() < selectedUnits.size() && (unitsThatCanMoveOnRoute.size() == 0 || Match.allMatch(unitsThatCanMoveOnRoute, Matches.UnitIsAir))) { final Collection<Unit> airUnits = Match.getMatches(selectedUnits, Matches.UnitIsAir); if (airUnits.size() > 0) { route = getRoute(getFirstSelectedTerritory(), territory, airUnits); updateUnitsThatCanMoveOnRoute(airUnits, route); } } } finally { getData().releaseReadLock(); } } else { route = routeCached; } mouseCurrentPoint = me.getMapPoint(); updateRouteAndMouseShadowUnits(route); } mouseCurrentTerritory = territory; } }; @Override public final String toString() { return "MovePanel"; } final void setFirstSelectedTerritory(final Territory firstSelectedTerritory) { if (this.firstSelectedTerritory == firstSelectedTerritory) { return; } this.firstSelectedTerritory = firstSelectedTerritory; if (firstSelectedTerritory == null) { mustMoveWithDetails = null; } else { mustMoveWithDetails = MoveValidator.getMustMoveWith(firstSelectedTerritory, firstSelectedTerritory.getUnits().getUnits(), s_dependentUnits, getData(), getCurrentPlayer()); } } private Territory getFirstSelectedTerritory() { return firstSelectedTerritory; } final void setSelectedEndpointTerritory(final Territory selectedEndpointTerritory) { this.selectedEndpointTerritory = selectedEndpointTerritory; } private Territory getSelectedEndpointTerritory() { return selectedEndpointTerritory; } private static boolean IsParatroopersCanMoveDuringNonCombat(final GameData data) { return games.strategy.triplea.Properties.getParatroopersCanMoveDuringNonCombat(data); } private final List<Unit> UserChooseUnits(final Set<Unit> defaultSelections, final Match<Collection<Unit>> unitsToLoadMatch, final List<Unit> unitsToLoad, final String title, final String action) { // Allow player to select which to load. final UnitChooser chooser = new UnitChooser(unitsToLoad, defaultSelections, s_dependentUnits, /* categorizeMovement */false, /* categorizeTransportCost */true, getGameData(), /* allowTwoHit */false, getMap().getUIContext(), unitsToLoadMatch); chooser.setTitle(title); final int option = JOptionPane.showOptionDialog(getTopLevelAncestor(), chooser, "What units do you want to " + action, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null); if (option != JOptionPane.OK_OPTION) { return Collections.emptyList(); } return chooser.getSelected(true); } @Override protected final void cleanUpSpecific() { getMap().removeMapSelectionListener(MAP_SELECTION_LISTENER); getMap().removeUnitSelectionListener(UNIT_SELECTION_LISTENER); getMap().removeMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); getMap().setUnitHighlight(null); selectedUnits.clear(); updateRouteAndMouseShadowUnits(null); forced = null; } @Override protected final void cancelMoveAction() { setFirstSelectedTerritory(null); setSelectedEndpointTerritory(null); mouseCurrentTerritory = null; forced = null; selectedUnits.clear(); currentCursorImage = null; updateRouteAndMouseShadowUnits(null); getMap().showMouseCursor(); getMap().setMouseShadowUnits(null); } @Override protected final void undoMoveSpecific() { getMap().setRoute(null); } public final void setNonCombat(final boolean nonCombat) { this.nonCombat = nonCombat; } public final void setDisplayText(final String displayText) { this.displayText = displayText; } @Override public final void display(final PlayerID id) { super.display(id, displayText); } @Override protected final void setUpSpecific() { setFirstSelectedTerritory(null); forced = null; getMap().addMapSelectionListener(MAP_SELECTION_LISTENER); getMap().addUnitSelectionListener(UNIT_SELECTION_LISTENER); getMap().addMouseOverUnitListener(MOUSE_OVER_UNIT_LISTENER); } public KeyListener getCustomKeyListeners() { return new KeyListener() { @Override public void keyTyped(final KeyEvent e) {} @Override public void keyPressed(final KeyEvent e) { switch (e.getKeyCode()) { case KeyEvent.VK_N: centerOnNextMoveableUnit(); break; case KeyEvent.VK_F: highlightMoveableUnits(); break; case KeyEvent.VK_U: if (getMap().getHighlightedUnits() != null && !getMap().getHighlightedUnits().isEmpty()) { m_undoableMovesPanel.undoMoves(getMap().getHighlightedUnits()); } break; } } @Override public void keyReleased(final KeyEvent e) {} }; } @Override protected boolean doneMoveAction() { if (m_undoableMovesPanel.getCountOfMovesMade() == 0) { final int rVal = JOptionPane.showConfirmDialog(JOptionPane.getFrameForComponent(MovePanel.this), "Are you sure you dont want to move?", "End Move", JOptionPane.YES_NO_OPTION); return rVal == JOptionPane.YES_OPTION; } return true; } @Override protected boolean setCancelButton() { return true; } private void centerOnNextMoveableUnit() { final List<Territory> allTerritories; getData().acquireReadLock(); try { allTerritories = new ArrayList<>(getData().getMap().getTerritories()); } finally { getData().releaseReadLock(); } final CompositeMatchAnd<Unit> moveableUnitOwnedByMe = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(getCurrentPlayer()), Matches.unitHasMovementLeft); if (!nonCombat) { // if not non combat, cannot move aa units moveableUnitOwnedByMe.add(Matches.UnitCanNotMoveDuringCombatMove.invert()); } final int size = allTerritories.size(); // new focused index is 1 greater int newFocusedIndex = lastFocusedTerritory == null ? 0 : allTerritories.indexOf(lastFocusedTerritory) + 1; if (newFocusedIndex >= size) { // if we are larger than the number of territories, we must start back at zero newFocusedIndex = 0; } Territory newFocusedTerritory = null; // make sure we go through every single territory on the board int i = 0; while (i < size) { final Territory t = allTerritories.get(newFocusedIndex); final List<Unit> matchedUnits = t.getUnits().getMatches(moveableUnitOwnedByMe); if (matchedUnits.size() > 0) { newFocusedTerritory = t; final Map<Territory, List<Unit>> highlight = new HashMap<>(); highlight.put(t, matchedUnits); getMap().setUnitHighlight(highlight); break; } // make sure to cycle through the front half of territories if ((newFocusedIndex + 1) >= size) { newFocusedIndex = 0; } else { newFocusedIndex++; } i++; } if (newFocusedTerritory != null) { lastFocusedTerritory = newFocusedTerritory; getMap().centerOn(newFocusedTerritory); } } private void highlightMoveableUnits() { final List<Territory> allTerritories; getData().acquireReadLock(); try { allTerritories = new ArrayList<>(getData().getMap().getTerritories()); } finally { getData().releaseReadLock(); } final CompositeMatchAnd<Unit> moveableUnitOwnedByMe = new CompositeMatchAnd<>(Matches.unitIsOwnedBy(getCurrentPlayer()), Matches.unitHasMovementLeft); if (!nonCombat) { // if not non combat, cannot move aa units moveableUnitOwnedByMe.add(Matches.UnitCanNotMoveDuringCombatMove.invert()); } final Map<Territory, List<Unit>> highlight = new HashMap<>(); for (final Territory t : allTerritories) { final List<Unit> moveableUnits = t.getUnits().getMatches(moveableUnitOwnedByMe); if (!moveableUnits.isEmpty()) { highlight.put(t, moveableUnits); } } if (!highlight.isEmpty()) { getMap().setUnitHighlight(highlight); } } }