package games.strategy.triplea.delegate; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; 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.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.changefactory.ChangeFactory; import games.strategy.triplea.Constants; import games.strategy.triplea.TripleAUnit; import games.strategy.triplea.attachments.UnitAttachment; import games.strategy.triplea.util.TransportUtils; /** * Tracks which transports are carrying which units. Also tracks the capacity * that has been unloaded. To reset the unloaded call clearUnloadedCapacity(). */ public class TransportTracker { public static int getCost(final Collection<Unit> units) { return TransportUtils.getTransportCost(units); } private static void assertTransport(final Unit u) { if (UnitAttachment.get(u.getType()).getTransportCapacity() == -1) { throw new IllegalStateException("Not a transport:" + u); } } /** * Constructor. */ private TransportTracker() {} /** * Returns the collection of units that the given transport is transporting. * Could be null. */ public static Collection<Unit> transporting(final Unit transport) { return new ArrayList<>(((TripleAUnit) transport).getTransporting()); } /** * Returns the collection of units that the given transport is transporting. * Could be null. */ public static Collection<Unit> transporting(final Unit transport, final Collection<Unit> transportedUnitsPossible) { return new ArrayList<>(((TripleAUnit) transport).getTransporting(transportedUnitsPossible)); } public static boolean isTransporting(final Unit transport) { return !((TripleAUnit) transport).getTransporting().isEmpty(); } /** * Returns the collection of units that the given transport has unloaded * this turn. Could be empty. */ public static Collection<Unit> unloaded(final Unit transport) { return ((TripleAUnit) transport).getUnloaded(); } public static Collection<Unit> transportingAndUnloaded(final Unit transport) { Collection<Unit> rVal = transporting(transport); if (rVal == null) { rVal = new ArrayList<>(); } rVal.addAll(unloaded(transport)); return rVal; } /** * Returns a map of transport -> collection of transported units. */ public static Map<Unit, Collection<Unit>> transporting(final Collection<Unit> units) { final Map<Unit, Collection<Unit>> returnVal = new HashMap<>(); for (final Unit transported : units) { final Unit transport = transportedBy(transported); Collection<Unit> transporting = null; if (transport != null) { transporting = transporting(transport); } if (transporting != null) { returnVal.put(transport, transporting); } } return returnVal; } /** * Returns a map of transport -> collection of transported units. */ public static Map<Unit, Collection<Unit>> transporting(final Collection<Unit> transports, final Collection<Unit> transportedUnits) { final Map<Unit, Collection<Unit>> returnVal = new HashMap<>(); for (final Unit transported : transportedUnits) { final Unit transport = transportedBy(transported); Collection<Unit> transporting = null; if (transport != null) { transporting = transporting(transport, transportedUnits); } if (transporting != null) { returnVal.put(transport, transporting); } } return returnVal; } /** * Return the transport that holds the given unit. Could be null. */ public static Unit transportedBy(final Unit unit) { return ((TripleAUnit) unit).getTransportedBy(); } public static Change unloadTransportChange(final TripleAUnit unit, final Territory territory, final PlayerID id, final boolean dependentBattle) { final CompositeChange change = new CompositeChange(); final TripleAUnit transport = (TripleAUnit) transportedBy(unit); if (transport == null) { return change; } assertTransport(transport); if (!transport.getTransporting().contains(unit)) { throw new IllegalStateException("Not being carried, unit:" + unit + " transport:" + transport); } final ArrayList<Unit> newUnloaded = new ArrayList<>(transport.getUnloaded()); newUnloaded.add(unit); change.add(ChangeFactory.unitPropertyChange(unit, territory, TripleAUnit.UNLOADED_TO)); if (!GameStepPropertiesHelper.isNonCombatMove(unit.getData(), true)) { change.add(ChangeFactory.unitPropertyChange(unit, true, TripleAUnit.UNLOADED_IN_COMBAT_PHASE)); change.add(ChangeFactory.unitPropertyChange(unit, true, TripleAUnit.UNLOADED_AMPHIBIOUS)); change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.UNLOADED_IN_COMBAT_PHASE)); change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.UNLOADED_AMPHIBIOUS)); } if (!dependentBattle) { // TODO: this is causing issues with Scrambling. if the units were unloaded, then scrambling creates a battle, // there is no longer any // way to have the units removed if those transports die. change.add(ChangeFactory.unitPropertyChange(unit, null, TripleAUnit.TRANSPORTED_BY)); } change.add(ChangeFactory.unitPropertyChange(transport, newUnloaded, TripleAUnit.UNLOADED)); return change; } public static Change unloadAirTransportChange(final TripleAUnit unit, final Territory territory, final PlayerID id, final boolean dependentBattle) { final CompositeChange change = new CompositeChange(); final TripleAUnit transport = (TripleAUnit) transportedBy(unit); if (transport == null) { return change; } assertTransport(transport); if (!transport.getTransporting().contains(unit)) { throw new IllegalStateException("Not being carried, unit:" + unit + " transport:" + transport); } final ArrayList<Unit> newUnloaded = new ArrayList<>(transport.getUnloaded()); newUnloaded.add(unit); change.add(ChangeFactory.unitPropertyChange(unit, territory, TripleAUnit.UNLOADED_TO)); if (!GameStepPropertiesHelper.isNonCombatMove(unit.getData(), true)) { change.add(ChangeFactory.unitPropertyChange(unit, true, TripleAUnit.UNLOADED_IN_COMBAT_PHASE)); // change.add(ChangeFactory.unitPropertyChange(unit, true, TripleAUnit.UNLOADED_AMPHIBIOUS)); change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.UNLOADED_IN_COMBAT_PHASE)); // change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.UNLOADED_AMPHIBIOUS)); } if (!dependentBattle) { // TODO: this is causing issues with Scrambling. if the units were unloaded, then scrambling creates a battle, // there is no longer any // way to have the units removed if those transports die. change.add(ChangeFactory.unitPropertyChange(unit, null, TripleAUnit.TRANSPORTED_BY)); } // dependencies for battle calc and casualty selection include unloaded. therefore even if we have unloaded this // unit, it will die if // air transport dies IF we have the unloaded flat set. so don't set it. // TODO: fix this bullshit by re-writing entire transportation engine // change.add(ChangeFactory.unitPropertyChange(transport, newUnloaded, TripleAUnit.UNLOADED)); return change; } public static Change loadTransportChange(final TripleAUnit transport, final Unit unit) { assertTransport(transport); final CompositeChange change = new CompositeChange(); // clear the loaded by change.add(ChangeFactory.unitPropertyChange(unit, transport, TripleAUnit.TRANSPORTED_BY)); final Collection<Unit> newCarrying = new ArrayList<>(transport.getTransporting()); if (newCarrying.contains(unit)) { throw new IllegalStateException("Already carrying, transport:" + transport + " unt:" + unit); } newCarrying.add(unit); change.add(ChangeFactory.unitPropertyChange(unit, Boolean.TRUE, TripleAUnit.LOADED_THIS_TURN)); change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.LOADED_THIS_TURN)); // If the transport was in combat, flag it as being loaded AFTER combat if (transport.getWasInCombat()) { change.add(ChangeFactory.unitPropertyChange(transport, true, TripleAUnit.LOADED_AFTER_COMBAT)); } return change; } public static int getAvailableCapacity(final Unit unit) { final UnitAttachment ua = UnitAttachment.get(unit.getType()); // Check if there are transports available, also check for destroyer capacity (Tokyo Express) if (ua.getTransportCapacity() == -1 || (unit.getData().getProperties().get(Constants.PACIFIC_THEATER, false) && ua.getIsDestroyer() && !unit.getOwner().getName().equals(Constants.PLAYER_NAME_JAPANESE))) { return 0; } final int capacity = ua.getTransportCapacity(); final int used = getCost(transporting(unit)); final int unloaded = getCost(unloaded(unit)); return capacity - used - unloaded; } public static Collection<Unit> getUnitsLoadedOnAlliedTransportsThisTurn(final Collection<Unit> units) { final Collection<Unit> rVal = new ArrayList<>(); for (final Unit u : units) { // a unit loaded onto an allied transport // cannot be unloaded in the same turn, so // if we check both wasLoadedThisTurn and // the transport that transports us, we can tell if // we were loaded onto an allied transport // if we are no longer being transported, // then we must have been transported on our own transport final TripleAUnit taUnit = (TripleAUnit) u; if (taUnit.getWasLoadedThisTurn() && taUnit.getTransportedBy() != null // an allied transport if the owner of the transport is not the owner of the unit && !taUnit.getTransportedBy().getOwner().equals(taUnit.getOwner())) { rVal.add(u); } } return rVal; } public static boolean hasTransportUnloadedInPreviousPhase(final Unit transport) { final Collection<Unit> unloaded = ((TripleAUnit) transport).getUnloaded(); // See if transport has unloaded anywhere yet for (final Unit u : unloaded) { final TripleAUnit taUnit = (TripleAUnit) u; // cannot unload in two different phases if (GameStepPropertiesHelper.isNonCombatMove(transport.getData(), true) && taUnit.getWasUnloadedInCombatPhase()) { return true; } } return false; } private static boolean isWW2V2(final GameData data) { return games.strategy.triplea.Properties.getWW2V2(data); } // TODO here's a bug COMCO private static boolean isTransportUnloadRestricted(final GameData data) { return games.strategy.triplea.Properties.getTransportUnloadRestricted(data); } // In some versions, a transport can never unload into // multiple territories in a given turn. // In WW2V1 a transport can unload to multiple territories in // non-combat phase, provided they are both adjacent to the sea zone. public static boolean isTransportUnloadRestrictedToAnotherTerritory(final Unit transport, final Territory territory) { final Collection<Unit> unloaded = ((TripleAUnit) transport).getUnloaded(); if (unloaded.isEmpty()) { return false; } // See if transport has unloaded anywhere yet final GameData data = transport.getData(); for (final Unit u : unloaded) { final TripleAUnit taUnit = (TripleAUnit) u; if (isWW2V2(data) || isTransportUnloadRestricted(data)) { // cannot unload to two different territories if (!taUnit.getUnloadedTo().equals(territory)) { return true; } } else { // cannot unload to two different territories in combat phase if (!GameStepPropertiesHelper.isNonCombatMove(transport.getData(), true) && !taUnit.getUnloadedTo().equals(territory)) { return true; } } } return false; } // This method should be called after isTransportUnloadRestrictedToAnotherTerritory() // returns false, in order to populate the error message. // However, we only need to call this method to determine why we can't // unload an additional unit. Since transports only hold up to two units, // we only need to return one territory, not multiple territories. public static Territory getTerritoryTransportHasUnloadedTo(final Unit transport) { final Collection<Unit> unloaded = ((TripleAUnit) transport).getUnloaded(); if (unloaded.isEmpty()) { return null; } final Iterator<Unit> iter = unloaded.iterator(); return ((TripleAUnit) iter.next()).getUnloadedTo(); } // If a transport has been in combat, it cannot load AND unload in non-combat public static boolean isTransportUnloadRestrictedInNonCombat(final Unit transport) { final TripleAUnit taUnit = (TripleAUnit) transport; return GameStepPropertiesHelper.isNonCombatMove(transport.getData(), true) && taUnit.getWasInCombat() && taUnit.getWasLoadedAfterCombat(); } }