package games.strategy.triplea.delegate;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
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.GameMap;
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.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.triplea.MapSupport;
import games.strategy.triplea.TripleAUnit;
import games.strategy.triplea.attachments.TechAbilityAttachment;
import games.strategy.triplea.delegate.IBattle.BattleType;
import games.strategy.triplea.delegate.dataObjects.MoveValidationResult;
import games.strategy.triplea.formatter.MyFormatter;
import games.strategy.util.CompositeMatchAnd;
import games.strategy.util.IntegerMap;
import games.strategy.util.Match;
/**
* SpecialMoveDelegate is a move delegate made for special movements like the new paratrooper/airborne movement.
* Airborne Attacks is actually Paratroopers tech for Global 1940, except that I really do not want to confuse myself by
* naming yet another
* thing Paratroopers, so this is now getting a new name.
* This is very different than "paratroopers" for AA50. We are actually launching the units from a static unit (an
* airbase) to another
* territory, instead of carrying them.
*/
@MapSupport
public class SpecialMoveDelegate extends AbstractMoveDelegate {
private boolean m_needToInitialize = true;
// private boolean m_allowAirborne = true;
public SpecialMoveDelegate() {}
/**
* Called before the delegate will run, AND before "start" is called.
*/
@Override
public void setDelegateBridgeAndPlayer(final IDelegateBridge iDelegateBridge) {
super.setDelegateBridgeAndPlayer(new GameDelegateBridge(iDelegateBridge));
}
@Override
public void start() {
super.start();
final GameData data = getData();
if (!allowAirborne(m_player, data)) {
return;
}
final boolean onlyWhereUnderAttackAlready =
games.strategy.triplea.Properties.getAirborneAttacksOnlyInExistingBattles(data);
final BattleTracker battleTracker = AbstractMoveDelegate.getBattleTracker(data);
if (m_needToInitialize && onlyWhereUnderAttackAlready) {
// we do this to clear any 'finishedBattles' and also to create battles for units that didn't move
BattleDelegate.doInitialize(battleTracker, m_bridge);
m_needToInitialize = false;
}
}
@Override
public void end() {
super.end();
m_needToInitialize = true;
}
@Override
public Serializable saveState() {
final SpecialMoveExtendedDelegateState state = new SpecialMoveExtendedDelegateState();
state.superState = super.saveState();
state.m_needToInitialize = m_needToInitialize;
return state;
}
@Override
public void loadState(final Serializable state) {
final SpecialMoveExtendedDelegateState s = (SpecialMoveExtendedDelegateState) state;
super.loadState(s.superState);
m_needToInitialize = s.m_needToInitialize;
}
@Override
public boolean delegateCurrentlyRequiresUserInput() {
return allowAirborne(m_player, getData());
}
@Override
public String move(final Collection<Unit> units, final Route route, final Collection<Unit> transportsThatCanBeLoaded,
final Map<Unit, Collection<Unit>> newDependents) {
if (!allowAirborne(m_player, getData())) {
return "No Airborne Movement Allowed Yet";
}
final GameData data = getData();
// there reason we use this, is because if we are in edit mode, we may have a different unit owner than the current
// player.
final PlayerID player = getUnitsOwner(units);
// here we have our own new validation method....
final MoveValidationResult result =
SpecialMoveDelegate.validateMove(units, route, player, transportsThatCanBeLoaded, newDependents,
GameStepPropertiesHelper.isNonCombatMove(data, false), m_movesToUndo, data);
final StringBuilder errorMsg = new StringBuilder(100);
final int numProblems = result.getTotalWarningCount() - (result.hasError() ? 0 : 1);
final String numErrorsMsg =
numProblems > 0 ? ("; " + numProblems + " " + MyFormatter.pluralize("error", numProblems) + " not shown") : "";
if (result.hasError()) {
return errorMsg.append(result.getError()).append(numErrorsMsg).toString();
}
if (result.hasDisallowedUnits()) {
return errorMsg.append(result.getDisallowedUnitWarning(0)).append(numErrorsMsg).toString();
}
if (result.hasUnresolvedUnits()) {
return errorMsg.append(result.getUnresolvedUnitWarning(0)).append(numErrorsMsg).toString();
}
// allow user to cancel move if aa guns will fire
final AAInMoveUtil aaInMoveUtil = new AAInMoveUtil();
aaInMoveUtil.initialize(m_bridge);
final Collection<Territory> aaFiringTerritores = aaInMoveUtil.getTerritoriesWhereAAWillFire(route, units);
if (!aaFiringTerritores.isEmpty()) {
if (!getRemotePlayer().confirmMoveInFaceOfAA(aaFiringTerritores)) {
return null;
}
}
// do the move
final UndoableMove currentMove = new UndoableMove(units, route);
// add dependencies (any move that came before this, from this start territory, is a dependency)
for (final UndoableMove otherMove : m_movesToUndo) {
if (otherMove.getStart().equals(route.getStart())) {
currentMove.addDependency(otherMove);
}
}
// make the units airborne
final CompositeChange airborneChange = new CompositeChange();
for (final Unit u : units) {
airborneChange.add(ChangeFactory.unitPropertyChange(u, true, TripleAUnit.AIRBORNE));
}
currentMove.addChange(airborneChange);
// make the bases start filling up their capacity
final Collection<Unit> basesAtStart = route.getStart().getUnits().getMatches(getAirborneBaseMatch(player, data));
final Change fillLaunchCapacity = getNewAssignmentOfNumberLaunchedChange(units.size(), basesAtStart, player, data);
currentMove.addChange(fillLaunchCapacity);
// start event
final String transcriptText = MyFormatter.unitsToTextNoOwner(units) + " moved from " + route.getStart().getName()
+ " to " + route.getEnd().getName();
m_bridge.getHistoryWriter().startEvent(transcriptText, currentMove.getDescriptionObject());
// actually do our special changes
m_bridge.addChange(airborneChange);
m_bridge.addChange(fillLaunchCapacity);
m_tempMovePerformer = new MovePerformer();
m_tempMovePerformer.initialize(this);
m_tempMovePerformer.moveUnits(units, route, player, transportsThatCanBeLoaded, newDependents, currentMove);
m_tempMovePerformer = null;
return null;
}
public static MoveValidationResult validateMove(final Collection<Unit> units, final Route route,
final PlayerID player, final Collection<Unit> transportsToLoad, final Map<Unit, Collection<Unit>> newDependents,
final boolean isNonCombat, final List<UndoableMove> undoableMoves, final GameData data) {
final MoveValidationResult result = new MoveValidationResult();
if (route.hasNoSteps()) {
return result;
}
if (MoveValidator.validateFirst(data, units, route, player, result).getError() != null) {
return result;
}
if (MoveValidator.validateFuel(data, units, route, player, result).getError() != null) {
return result;
}
final boolean isEditMode = getEditMode(data);
if (!isEditMode) {
// make sure all units are at least friendly
for (final Unit unit : Match.getMatches(units, Matches.unitIsOwnedBy(player).invert())) {
result.addDisallowedUnit("Can only move owned units", unit);
}
}
if (validateAirborneMovements(data, units, route, player, result).getError() != null) {
return result;
}
return result;
}
private static MoveValidationResult validateAirborneMovements(final GameData data, final Collection<Unit> units,
final Route route, final PlayerID player, final MoveValidationResult result) {
if (!TechAbilityAttachment.getAllowAirborneForces(player, data)) {
return result.setErrorReturnResult("Do Not Have Airborne Tech");
}
final int airborneDistance = TechAbilityAttachment.getAirborneDistance(player, data);
final Set<UnitType> airborneBases = TechAbilityAttachment.getAirborneBases(player, data);
final Set<UnitType> airborneTypes = TechAbilityAttachment.getAirborneTypes(player, data);
if (airborneDistance <= 0 || airborneBases.isEmpty() || airborneTypes.isEmpty()) {
return result.setErrorReturnResult("Require Airborne Forces And Launch Capacity Tech");
}
if (route.numberOfSteps() > airborneDistance) {
return result.setErrorReturnResult("Destination Is Out Of Range");
}
final Collection<PlayerID> alliesForBases = data.getRelationshipTracker().getAllies(player, true);
final Match<Unit> airborneBaseMatch = getAirborneMatch(airborneBases, alliesForBases);
final Territory start = route.getStart();
final Territory end = route.getEnd();
final Collection<Unit> basesAtStart = start.getUnits().getMatches(airborneBaseMatch);
if (basesAtStart.isEmpty()) {
return result.setErrorReturnResult("Require Airborne Base At Originating Territory");
}
final int airborneCapacity = TechAbilityAttachment.getAirborneCapacity(basesAtStart, player, data);
if (airborneCapacity <= 0) {
return result.setErrorReturnResult("Airborne Bases Must Have Launch Capacity");
} else if (airborneCapacity < units.size()) {
final Collection<Unit> overMax = new ArrayList<>(units);
overMax.removeAll(Match.getNMatches(units, airborneCapacity, Match.getAlwaysMatch()));
for (final Unit u : overMax) {
result.addDisallowedUnit("Airborne Base Capacity Has Been Reached", u);
}
}
final Collection<Unit> airborne = new ArrayList<>();
for (final Unit u : units) {
if (!Matches.unitIsOwnedBy(player).match(u)) {
result.addDisallowedUnit("Must Own All Airborne Forces", u);
} else if (!Matches.unitIsOfTypes(airborneTypes).match(u)) {
result.addDisallowedUnit("Can Only Launch Airborne Forces", u);
} else if (Matches.UnitIsDisabled.match(u)) {
result.addDisallowedUnit("Must Not Be Disabled", u);
} else if (!Matches.unitHasNotMoved.match(u)) {
result.addDisallowedUnit("Must Not Have Previously Moved Airborne Forces", u);
} else if (Matches.UnitIsAirborne.match(u)) {
result.addDisallowedUnit("Cannot Move Units Already Airborne", u);
} else {
airborne.add(u);
}
}
if (airborne.isEmpty()) {
return result;
}
final BattleTracker battleTracker = AbstractMoveDelegate.getBattleTracker(data);
final boolean onlyWhereUnderAttackAlready =
games.strategy.triplea.Properties.getAirborneAttacksOnlyInExistingBattles(data);
final boolean onlyEnemyTerritories =
games.strategy.triplea.Properties.getAirborneAttacksOnlyInEnemyTerritories(data);
if (!Match.allMatch(route.getSteps(), Matches.TerritoryIsPassableAndNotRestricted(player, data))) {
return result.setErrorReturnResult("May Not Fly Over Impassable or Restricted Territories");
}
if (!Match.allMatch(route.getSteps(), Matches.TerritoryAllowsCanMoveAirUnitsOverOwnedLand(player, data))) {
return result.setErrorReturnResult("May Only Fly Over Territories Where Air May Move");
}
final boolean someLand = Match.someMatch(airborne, Matches.UnitIsLand);
final boolean someSea = Match.someMatch(airborne, Matches.UnitIsSea);
final boolean land = Matches.TerritoryIsLand.match(end);
final boolean sea = Matches.TerritoryIsWater.match(end);
if (someLand && someSea) {
return result.setErrorReturnResult("Cannot Mix Land and Sea Units");
} else if (someLand) {
if (!land) {
return result.setErrorReturnResult("Cannot Move Land Units To Sea");
}
} else if (someSea) {
if (!sea) {
return result.setErrorReturnResult("Cannot Move Sea Units To Land");
}
}
if (onlyWhereUnderAttackAlready) {
if (!battleTracker.getConquered().contains(end)) {
final IBattle battle = battleTracker.getPendingBattle(end, false, BattleType.NORMAL);
if (battle == null) {
return result.setErrorReturnResult("Airborne May Only Attack Territories Already Under Assault");
} else if (land && someLand && !Match.someMatch(battle.getAttackingUnits(), Matches.UnitIsLand)) {
return result.setErrorReturnResult("Battle Must Have Some Land Units Participating Already");
} else if (sea && someSea && !Match.someMatch(battle.getAttackingUnits(), Matches.UnitIsSea)) {
return result.setErrorReturnResult("Battle Must Have Some Sea Units Participating Already");
}
}
} else if (onlyEnemyTerritories) {
if (!(Matches.isTerritoryEnemyAndNotUnownedWater(player, data).match(end)
|| Matches.territoryHasEnemyUnits(player, data).match(end))) {
return result.setErrorReturnResult("Destination Must Be Enemy Or Contain Enemy Units");
}
}
return result;
}
private static Match<Unit> getAirborneBaseMatch(final PlayerID player, final GameData data) {
return getAirborneMatch(TechAbilityAttachment.getAirborneBases(player, data),
data.getRelationshipTracker().getAllies(player, true));
}
private static Match<Unit> getAirborneMatch(final Set<UnitType> types, final Collection<PlayerID> unitOwners) {
return new CompositeMatchAnd<>(Matches.unitIsOwnedByOfAnyOfThesePlayers(unitOwners),
Matches.unitIsOfTypes(types), Matches.UnitIsNotDisabled, Matches.unitHasNotMoved,
Matches.UnitIsAirborne.invert());
}
private static Change getNewAssignmentOfNumberLaunchedChange(int newNumberLaunched, final Collection<Unit> bases,
final PlayerID player, final GameData data) {
final CompositeChange launchedChange = new CompositeChange();
if (newNumberLaunched <= 0) {
return launchedChange;
}
final IntegerMap<UnitType> capacityMap = TechAbilityAttachment.getAirborneCapacity(player, data);
for (final Unit u : bases) {
if (newNumberLaunched <= 0) {
break;
}
final int numberLaunchedAlready = ((TripleAUnit) u).getLaunched();
final int capacity = capacityMap.getInt(u.getType());
final int toAdd = Math.min(newNumberLaunched, capacity - numberLaunchedAlready);
if (toAdd <= 0) {
continue;
}
newNumberLaunched -= toAdd;
launchedChange.add(ChangeFactory.unitPropertyChange(u, (toAdd + numberLaunchedAlready), TripleAUnit.LAUNCHED));
}
return launchedChange;
}
private static boolean allowAirborne(final PlayerID player, final GameData data) {
if (!TechAbilityAttachment.getAllowAirborneForces(player, data)) {
return false;
}
final int airborneDistance = TechAbilityAttachment.getAirborneDistance(player, data);
final Set<UnitType> airborneBases = TechAbilityAttachment.getAirborneBases(player, data);
final Set<UnitType> airborneTypes = TechAbilityAttachment.getAirborneTypes(player, data);
if (airborneDistance <= 0 || airborneBases.isEmpty() || airborneTypes.isEmpty()) {
return false;
}
final GameMap map = data.getMap();
final Collection<PlayerID> alliesForBases = data.getRelationshipTracker().getAllies(player, true);
final Collection<Territory> territoriesWeCanLaunchFrom = Match.getMatches(map.getTerritories(),
Matches.territoryHasUnitsThatMatch(getAirborneMatch(airborneBases, alliesForBases)));
return !territoriesWeCanLaunchFrom.isEmpty();
}
private static boolean getEditMode(final GameData data) {
return BaseEditDelegate.getEditMode(data);
}
@Override
public int PUsAlreadyLost(final Territory t) {
// Auto-generated method stub
return 0;
}
@Override
public void PUsLost(final Territory t, final int amt) {
// Auto-generated method stub
}
}
class SpecialMoveExtendedDelegateState implements Serializable {
private static final long serialVersionUID = 7781410008392307104L;
Serializable superState;
public boolean m_needToInitialize;
}