mirror of https://github.com/FreeCol/freecol.git
4969 lines
165 KiB
Java
4969 lines
165 KiB
Java
/**
|
|
* Copyright (C) 2002-2022 The FreeCol Team
|
|
*
|
|
* This file is part of FreeCol.
|
|
*
|
|
* FreeCol is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* FreeCol is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package net.sf.freecol.common.model;
|
|
|
|
import static net.sf.freecol.common.model.Constants.INFINITY;
|
|
import static net.sf.freecol.common.model.Constants.UNDEFINED;
|
|
import static net.sf.freecol.common.util.CollectionUtils.alwaysTrue;
|
|
import static net.sf.freecol.common.util.CollectionUtils.any;
|
|
import static net.sf.freecol.common.util.CollectionUtils.cachingIntComparator;
|
|
import static net.sf.freecol.common.util.CollectionUtils.concat;
|
|
import static net.sf.freecol.common.util.CollectionUtils.find;
|
|
import static net.sf.freecol.common.util.CollectionUtils.first;
|
|
import static net.sf.freecol.common.util.CollectionUtils.flatten;
|
|
import static net.sf.freecol.common.util.CollectionUtils.minimize;
|
|
import static net.sf.freecol.common.util.CollectionUtils.none;
|
|
import static net.sf.freecol.common.util.CollectionUtils.rotate;
|
|
import static net.sf.freecol.common.util.CollectionUtils.sum;
|
|
import static net.sf.freecol.common.util.CollectionUtils.transform;
|
|
import static net.sf.freecol.common.util.StringUtils.getEnumKey;
|
|
import static net.sf.freecol.common.util.StringUtils.lastPart;
|
|
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
import javax.xml.stream.XMLStreamException;
|
|
|
|
import net.sf.freecol.common.i18n.Messages;
|
|
import net.sf.freecol.common.io.FreeColXMLReader;
|
|
import net.sf.freecol.common.io.FreeColXMLWriter;
|
|
import net.sf.freecol.common.model.Constants.IntegrityType;
|
|
import net.sf.freecol.common.model.pathfinding.CostDecider;
|
|
import net.sf.freecol.common.model.pathfinding.CostDeciders;
|
|
import net.sf.freecol.common.model.pathfinding.GoalDecider;
|
|
import net.sf.freecol.common.model.pathfinding.GoalDeciders;
|
|
import net.sf.freecol.common.option.GameOptions;
|
|
import net.sf.freecol.common.util.LogBuilder;
|
|
|
|
|
|
/**
|
|
* Represents all pieces that can be moved on the map-board. This includes:
|
|
* colonists, ships, wagon trains e.t.c.
|
|
*
|
|
* Every {@code Unit} is owned by a {@link Player} and has a
|
|
* {@link Location}.
|
|
*/
|
|
public class Unit extends GoodsLocation
|
|
implements Consumer, Locatable, Movable, Nameable, Ownable {
|
|
|
|
private static final Logger logger = Logger.getLogger(Unit.class.getName());
|
|
|
|
private static class ClosestSettlementGoalDecider implements GoalDecider {
|
|
|
|
/** A tile to exclude. */
|
|
private Tile exclude;
|
|
|
|
/** Require a connected port. */
|
|
private boolean coastal;
|
|
|
|
/** Best value so far. */
|
|
private int bestValue;
|
|
|
|
/** Best path so far. */
|
|
private PathNode best;
|
|
|
|
|
|
/**
|
|
* Build a new goal decider to find the closest path to a settlement
|
|
* owned by the player controlling the searching unit.
|
|
*
|
|
* @param exclude An optional tile to exclude.
|
|
* @param coastal If true, a connected port is required.
|
|
*/
|
|
public ClosestSettlementGoalDecider(Tile exclude, boolean coastal) {
|
|
this.exclude = exclude;
|
|
this.coastal = coastal;
|
|
this.bestValue = Integer.MAX_VALUE;
|
|
this.best = null;
|
|
}
|
|
|
|
// Implement GoalDecider
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public PathNode getGoal() {
|
|
return this.best;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public boolean hasSubGoals() {
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public boolean check(Unit u, PathNode path) {
|
|
Tile t = path.getTile();
|
|
if (t == null || t == this.exclude) return false;
|
|
Settlement settlement = t.getSettlement();
|
|
int value;
|
|
if (settlement != null
|
|
&& u.getOwner().owns(settlement)
|
|
&& (!this.coastal || settlement.isConnectedPort())
|
|
&& (value = path.getTotalTurns()) < bestValue) {
|
|
bestValue = value;
|
|
best = path;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/** Class index for units. */
|
|
private static final int UNIT_CLASS_INDEX = 40;
|
|
|
|
public static final String TAG = "unit";
|
|
|
|
/**
|
|
* A large number of turns, denoting pathfinding failure. Do not use
|
|
* INFINITY as further calculation might use this.
|
|
*/
|
|
public static final int MANY_TURNS = 10000;
|
|
|
|
/** Default value of unpriced units, used in evaluate_for. */
|
|
public static final int DEFAULT_UNIT_VALUE = 500;
|
|
|
|
public static final String CARGO_CHANGE = "CARGO_CHANGE";
|
|
public static final String MOVE_CHANGE = "MOVE_CHANGE";
|
|
public static final String ROLE_CHANGE = "ROLE_CHANGE";
|
|
|
|
/** Compare units by location. */
|
|
public static final Comparator<Unit> locComparator
|
|
= Comparator.comparingInt(u -> Location.rankOf(u));
|
|
|
|
/**
|
|
* A comparator to compare units by type, then role index, then
|
|
* the FCO order.
|
|
*/
|
|
public static final Comparator<Unit> typeRoleComparator
|
|
= Comparator.comparing(Unit::getType)
|
|
.thenComparingInt(u -> u.getRole().getRoleIndex())
|
|
.thenComparing(FreeColObject.fcoComparator);
|
|
|
|
/** A comparator to compare units by increasing skill level. */
|
|
public static final Comparator<Unit> increasingSkillComparator
|
|
= Comparator.comparingInt(Unit::getSkillLevel);
|
|
/** A comparator to compare units by decreasing skill level. */
|
|
public static final Comparator<Unit> decreasingSkillComparator
|
|
= increasingSkillComparator.reversed();
|
|
|
|
/**
|
|
* Comparator to rank settlements by accessibility by sea to Europe.
|
|
*/
|
|
private static final Comparator<Settlement> settlementStartComparator
|
|
= cachingIntComparator(s ->
|
|
(s == null || !s.getTile().isHighSeasConnected()) ? INFINITY
|
|
: s.getTile().getHighSeasCount());
|
|
|
|
/** Useful predicate for finding sentried land units. */
|
|
public static final Predicate<Unit> sentryPred = u ->
|
|
!u.isNaval() && u.getState() == UnitState.SENTRY;
|
|
|
|
/** A state a Unit can have. */
|
|
public enum UnitState {
|
|
ACTIVE,
|
|
FORTIFIED,
|
|
SENTRY,
|
|
IN_COLONY,
|
|
IMPROVING,
|
|
FORTIFYING,
|
|
SKIPPED;
|
|
|
|
/**
|
|
* Get the stem key for this unit state.
|
|
*
|
|
* @return The stem key.
|
|
*/
|
|
public String getKey() {
|
|
return "unitState." + getEnumKey(this);
|
|
}
|
|
}
|
|
|
|
/** Internal state for findIntermediatePort. */
|
|
private enum PortMode {
|
|
LAKE,
|
|
NO_HIGH_SEAS,
|
|
BLOCKED,
|
|
LAND
|
|
};
|
|
|
|
|
|
/** The individual name of this unit, not of the unit type. */
|
|
protected String name = null;
|
|
|
|
/** The owner player. */
|
|
protected Player owner;
|
|
|
|
/** The unit type. */
|
|
protected UnitType type;
|
|
|
|
/** Current unit state. */
|
|
protected UnitState state = UnitState.ACTIVE;
|
|
|
|
/** Current unit role. */
|
|
protected Role role;
|
|
|
|
/**
|
|
* The amount of role-equipment this unit carries, subject to
|
|
* role.getMaximumCount(). Currently zero or one except for pioneers.
|
|
*/
|
|
protected int roleCount;
|
|
|
|
/** The current unit location. */
|
|
protected Location location;
|
|
|
|
/** The last entry location used by this unit. */
|
|
protected Location entryLocation;
|
|
|
|
/** The number of moves this unit has left this turn. */
|
|
protected int movesLeft;
|
|
|
|
/** What type of goods this unit produces in its occupation. */
|
|
protected GoodsType workType;
|
|
|
|
/** What type of goods this unit last earned experience producing. */
|
|
protected GoodsType experienceType;
|
|
|
|
/** The mount of experience a unit has earned. */
|
|
protected int experience = 0;
|
|
|
|
/**
|
|
* The number of turns until the work is finished (e.g. sailing,
|
|
* improving), or '-1' if a Unit can stay in its state forever.
|
|
*/
|
|
protected int workLeft;
|
|
|
|
/**
|
|
* What is being improved (to be used only for PIONEERs - where
|
|
* they are working.
|
|
*/
|
|
protected TileImprovement workImprovement;
|
|
|
|
/** The student of this Unit, if it has one. */
|
|
protected Unit student;
|
|
|
|
/** The teacher of this Unit, if it has one. */
|
|
protected Unit teacher;
|
|
|
|
/** Number of turns of training needed by this unit. */
|
|
protected int turnsOfTraining = 0;
|
|
|
|
/** The original nationality. */
|
|
protected String nationality = null;
|
|
|
|
/** The original ethnicity. */
|
|
protected String ethnicity = null;
|
|
|
|
/** The home settlement of a native unit. */
|
|
protected IndianSettlement indianSettlement = null;
|
|
|
|
/** For now; only used by ships when repairing. */
|
|
protected int hitPoints;
|
|
|
|
/** A destination for go-to moves. */
|
|
protected Location destination = null;
|
|
|
|
/** The trade route this unit has. */
|
|
protected TradeRoute tradeRoute = null;
|
|
|
|
/** Which stop in a trade route the unit is going to. */
|
|
protected int currentStop = -1;
|
|
|
|
/** To be used only for type == TREASURE_TRAIN */
|
|
protected int treasureAmount;
|
|
|
|
/**
|
|
* The attrition this unit has accumulated. At the moment, this
|
|
* equals the number of turns it has spent in the open.
|
|
*/
|
|
protected int attrition = 0;
|
|
|
|
/**
|
|
* The amount of goods carried by this unit. This variable is
|
|
* only used by the clients. A negative value signals that the
|
|
* variable is not in use.
|
|
*
|
|
* @see #getVisibleGoodsCount()
|
|
*/
|
|
protected int visibleGoodsCount;
|
|
|
|
|
|
/**
|
|
* Constructor for ServerUnit.
|
|
*
|
|
* @param game The enclosing {@code Game}.
|
|
*/
|
|
protected Unit(Game game) {
|
|
super(game);
|
|
|
|
initialize();
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@code Unit} with the given
|
|
* identifier. The object should later be initialized by calling
|
|
* {@link #readFromXML(FreeColXMLReader)}.
|
|
*
|
|
* @param game The enclosing {@code Game}.
|
|
* @param id The object identifier.
|
|
*/
|
|
public Unit(Game game, String id) {
|
|
super(game, id);
|
|
|
|
initialize();
|
|
}
|
|
|
|
|
|
/**
|
|
* Initialize the nationality and ethnicity.
|
|
*/
|
|
private final void initialize() {
|
|
Player owner = getOwner();
|
|
if (owner != null && isPerson()) {
|
|
setNationality(owner.getNationId());
|
|
setEthnicity(owner.getNationId());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the individual name of this unit.
|
|
*
|
|
* @return The individual name.
|
|
*/
|
|
@Override
|
|
public String getName() {
|
|
return name;
|
|
}
|
|
|
|
/**
|
|
* Set the individual name of this unit.
|
|
*
|
|
* @param newName The new name.
|
|
*/
|
|
@Override
|
|
public void setName(String newName) {
|
|
this.name = newName;
|
|
}
|
|
|
|
/**
|
|
* Get the name of the apparent owner of this Unit,
|
|
* (like getOwner().getNationAsString() but handles pirates).
|
|
*
|
|
* @return The name of the apparent owner of this {@code Unit}.
|
|
*/
|
|
public StringTemplate getApparentOwnerName() {
|
|
Player own = (isOwnerHidden()) ? getGame().getUnknownEnemy()
|
|
: owner;
|
|
return own.getNationLabel();
|
|
}
|
|
|
|
/** What type of unit label do we want? */
|
|
public enum UnitLabelType {
|
|
PLAIN, // Just the basics
|
|
NATIONAL, // Add the nation
|
|
FULL // Add the equipment and extras
|
|
}
|
|
|
|
/**
|
|
* Get a plain string template for a unit.
|
|
*
|
|
* @return The {@code StringTemplate} to describe the given unit.
|
|
*/
|
|
public StringTemplate getLabel() {
|
|
return getLabel(UnitLabelType.PLAIN);
|
|
}
|
|
|
|
/**
|
|
* Get a string template for a unit.
|
|
*
|
|
* The PLAIN template contains:
|
|
* - The type of the unit
|
|
* - A role if not the default
|
|
* - The specific name of the unit if it has one
|
|
* The NATIONAL template adds the nation
|
|
* The FULL template adds equipment annotations
|
|
*
|
|
* @param ult The type of label to get.
|
|
* @return The {@code StringTemplate} to describe the given unit.
|
|
*/
|
|
public StringTemplate getLabel(UnitLabelType ult) {
|
|
final UnitType type = getType();
|
|
final Role role = getRole();
|
|
final Player owner = getOwner();
|
|
if (type == null || role == null || owner == null) {
|
|
return null; // Probably disposed
|
|
}
|
|
|
|
switch (ult) {
|
|
case PLAIN:
|
|
return Messages.getUnitLabel(getName(), type.getId(), 1, null,
|
|
role.getId(), null);
|
|
|
|
case NATIONAL:
|
|
if (role.getMaximumCount() > 1) {
|
|
// If the amount of equipment can vary an equipment
|
|
// label is required, so fall through into the FULL case.
|
|
} else {
|
|
return Messages.getUnitLabel(getName(), type.getId(), 1,
|
|
owner.getNationId(), role.getId(),
|
|
null);
|
|
}
|
|
// Fall through
|
|
|
|
case FULL:
|
|
StringTemplate extra = null;
|
|
if (role.isDefaultRole()) {
|
|
if (canCarryTreasure()) {
|
|
extra = StringTemplate.template("goldAmount")
|
|
.addAmount("%amount%", getTreasureAmount());
|
|
} else {
|
|
boolean noEquipment = false;
|
|
// unequipped expert has no-equipment label
|
|
List<Role> expertRoles = type.getExpertRoles();
|
|
for (Role someRole : expertRoles) {
|
|
String key = someRole.getId() + ".noequipment";
|
|
if (Messages.containsKey(key)) {
|
|
extra = StringTemplate.key(key);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
String equipmentKey = role.getId() + ".equipment";
|
|
if (Messages.containsKey(equipmentKey)) {
|
|
// Currently only used for missionary which does not
|
|
// have equipment that directly corresponds to goods.
|
|
extra = AbstractGoods.getAbstractLabel(equipmentKey, 1);
|
|
} else {
|
|
// Other roles can be characterized by their goods.
|
|
List<AbstractGoods> requiredGoods
|
|
= role.getRequiredGoodsList(getRoleCount());
|
|
boolean first = true;
|
|
extra = StringTemplate.label("");
|
|
for (AbstractGoods ag : requiredGoods) {
|
|
if (first) first = false; else extra.addName(" ");
|
|
extra.addStringTemplate(ag.getLabel());
|
|
}
|
|
}
|
|
}
|
|
return Messages.getUnitLabel(getName(), type.getId(), 1,
|
|
owner.getNationId(), role.getId(),
|
|
extra);
|
|
default: // Can not happen
|
|
break;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get the basic i18n description for this unit.
|
|
*
|
|
* @return A {@code String} describing this unit.
|
|
*/
|
|
public String getDescription() {
|
|
return Messages.message(getLabel());
|
|
}
|
|
|
|
/**
|
|
* Get the basic i18n description for this unit.
|
|
*
|
|
* @param ult The label type required.
|
|
* @return A {@code String} describing this unit.
|
|
*/
|
|
public String getDescription(UnitLabelType ult) {
|
|
return Messages.message(getLabel(ult));
|
|
}
|
|
|
|
/**
|
|
* Get a label for the chance of success in a potential combat.
|
|
*
|
|
* @param tile The {@code Tile} to attack into.
|
|
* @return A suitable label.
|
|
*/
|
|
public StringTemplate getCombatLabel(Tile tile) {
|
|
final CombatModel.CombatOdds combatOdds = getGame().getCombatModel()
|
|
.calculateCombatOdds(this, tile.getDefendingUnit(this));
|
|
// If attacking a settlement, the true odds are never
|
|
// known because units may be hidden within
|
|
boolean unknown = combatOdds.win == CombatModel.CombatOdds.UNKNOWN_ODDS
|
|
|| tile.hasSettlement();
|
|
return StringTemplate.template("model.unit.attackTileOdds")
|
|
.addName("%chance%", (unknown) ? "??"
|
|
: String.valueOf((int)(combatOdds.win * 100)));
|
|
}
|
|
|
|
/**
|
|
* Get a destination label for this unit.
|
|
*
|
|
* @return A {@code StringTemplate} describing where this unit
|
|
* is going.
|
|
*/
|
|
public StringTemplate getDestinationLabel() {
|
|
// Create the right tag for the tagged "goingTo" message.
|
|
String type = (isPerson()) ? "person"
|
|
: (isNaval()) ? "ship"
|
|
: "other";
|
|
return getUnitDestinationLabel(type, getDestination(), getOwner());
|
|
}
|
|
|
|
/**
|
|
* Get a destination label for a given unit tag, destination and player.
|
|
*
|
|
* @param tag The unit tag for the "goingTo" message.
|
|
* @param destination The destination {@code Location}.
|
|
* @param player The {@code Player} viewpoint.
|
|
* @return A {@code StringTemplate} describing the unit movement.
|
|
*/
|
|
public static StringTemplate getUnitDestinationLabel(String tag,
|
|
Location destination, Player player) {
|
|
return StringTemplate.template("model.unit.goingTo")
|
|
.addTagged("%type%", tag)
|
|
.addStringTemplate("%location%",
|
|
destination.getLocationLabelFor(player));
|
|
}
|
|
|
|
/**
|
|
* Get a string template describing the repair state of this unit.
|
|
*
|
|
* @return A repair label.
|
|
*/
|
|
public StringTemplate getRepairLabel() {
|
|
return StringTemplate.template("model.unit.underRepair")
|
|
.addAmount("%turns%", getTurnsForRepair());
|
|
}
|
|
|
|
/**
|
|
* Get the {@code UnitType} value.
|
|
*
|
|
* @return The current {@code UnitType}.
|
|
*/
|
|
public final UnitType getType() {
|
|
return this.type;
|
|
}
|
|
|
|
/**
|
|
* Sets the type of the unit.
|
|
*
|
|
* -vis: Has visibility issues as the line of sight may change.
|
|
*
|
|
* @param type The new type of the unit.
|
|
*/
|
|
public void setType(UnitType type) {
|
|
this.type = type;
|
|
}
|
|
|
|
/**
|
|
* Changes the type of the unit.
|
|
*
|
|
* -vis: Has visibility issues as the line of sight may change.
|
|
*
|
|
* @param unitType The new type of the unit.
|
|
* @return True if the type change succeeds.
|
|
*/
|
|
public boolean changeType(UnitType unitType) {
|
|
if (!unitType.isAvailableTo(owner)) return false;
|
|
|
|
final double health = ((double) getHitPoints()) / getMaximumHitPoints();
|
|
setType(unitType);
|
|
if (getMovesLeft() > getInitialMovesLeft()) {
|
|
setMovesLeft(getInitialMovesLeft());
|
|
}
|
|
this.hitPoints = (int) (unitType.getHitPoints() * health);
|
|
if (getTeacher() != null) {
|
|
if (!canBeStudent(getTeacher())) {
|
|
getTeacher().setStudent(null);
|
|
setTeacher(null);
|
|
}
|
|
} else if (getStudent() != null) {
|
|
if (!getStudent().canBeStudent(this)) {
|
|
getStudent().setTeacher(null);
|
|
setStudent(null);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Score this unit.
|
|
*
|
|
* Just delegates to unit type for now.
|
|
*
|
|
* @return The score for this unit.
|
|
*/
|
|
public int getScoreValue() {
|
|
return (this.type == null) ? 0 : this.type.getScoreValue();
|
|
}
|
|
|
|
/**
|
|
* Checks if this {@code Unit} is naval.
|
|
*
|
|
* @return True if this is a naval {@code Unit}.
|
|
*/
|
|
public boolean isNaval() {
|
|
return (this.type == null) ? false : this.type.isNaval();
|
|
}
|
|
|
|
/**
|
|
* Is this a unit that hides its ownership?
|
|
*
|
|
* @return True if the owner should be hidden from clients.
|
|
*/
|
|
public boolean isOwnerHidden() {
|
|
return (this.type == null) ? false
|
|
: this.type.hasAbility(Ability.PIRACY);
|
|
}
|
|
|
|
/**
|
|
* Checks if this unit is an undead.
|
|
*
|
|
* @return True if the unit is undead.
|
|
*/
|
|
public boolean isUndead() {
|
|
return hasAbility(Ability.UNDEAD);
|
|
}
|
|
|
|
/**
|
|
* Can this unit carry treasure (like a treasure train)?
|
|
*
|
|
* @return True if this {@code Unit} can carry treasure.
|
|
*/
|
|
public boolean canCarryTreasure() {
|
|
return hasAbility(Ability.CARRY_TREASURE);
|
|
}
|
|
|
|
/**
|
|
* Can this unit capture enemy goods?
|
|
*
|
|
* @return True if this {@code Unit} is capable of capturing goods.
|
|
*/
|
|
public boolean canCaptureGoods() {
|
|
return hasAbility(Ability.CAPTURE_GOODS);
|
|
}
|
|
|
|
/**
|
|
* Checks if this is a trading {@code Unit}, meaning that it
|
|
* can trade with settlements.
|
|
*
|
|
* @return True if this is a trading unit.
|
|
*/
|
|
public boolean isTradingUnit() {
|
|
return canCarryGoods() && owner.isEuropean();
|
|
}
|
|
|
|
/**
|
|
* Checks if this {@code Unit} is a `colonist'. A unit is a
|
|
* colonist if it is European and can build a new {@code Colony}.
|
|
*
|
|
* @return True if this unit is a colonist.
|
|
*/
|
|
public boolean isColonist() {
|
|
return this.type.hasAbility(Ability.FOUND_COLONY)
|
|
&& owner.hasAbility(Ability.FOUNDS_COLONIES);
|
|
}
|
|
|
|
/**
|
|
* Checks if this {@code Unit} is able to carry {@link Locatable}s.
|
|
*
|
|
* @return True if this unit can carry goods or other units.
|
|
*/
|
|
public boolean isCarrier() {
|
|
return this.type.canCarryGoods() || this.type.canCarryUnits();
|
|
}
|
|
|
|
/**
|
|
* Checks if this unit is a person, that is not a ship or wagon.
|
|
*
|
|
* @return True if this unit is a person.
|
|
*/
|
|
public boolean isPerson() {
|
|
return (this.type == null) ? false
|
|
: this.type.hasAbility(Ability.PERSON);
|
|
}
|
|
|
|
/**
|
|
* Gets the state of this {@code Unit}.
|
|
*
|
|
* @return The state of this {@code Unit}.
|
|
*/
|
|
public UnitState getState() {
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Checks if a {@code Unit} can get the given state set.
|
|
*
|
|
* @param s The new state for this Unit. Should be one of
|
|
* {UnitState.ACTIVE, FORTIFIED, ...}.
|
|
* @return True if the {@code Unit} state can be changed to
|
|
* the new value.
|
|
*/
|
|
public boolean checkSetState(UnitState s) {
|
|
if (getState() == s) return false;
|
|
switch (s) {
|
|
case ACTIVE:
|
|
return true;
|
|
case FORTIFIED:
|
|
return getState() == UnitState.FORTIFYING;
|
|
case FORTIFYING:
|
|
return getMovesLeft() > 0;
|
|
case IMPROVING:
|
|
return getMovesLeft() > 0 && isOnTile()
|
|
&& getOwner().canAcquireForImprovement(getLocation().getTile());
|
|
case IN_COLONY:
|
|
return !isNaval();
|
|
case SENTRY:
|
|
return true;
|
|
case SKIPPED:
|
|
return getState() == UnitState.ACTIVE;
|
|
default:
|
|
logger.warning("Invalid unit state: " + s);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a new state for this unit and initializes the amount of
|
|
* work the unit has left.
|
|
*
|
|
* If the work needs turns to be completed (for instance when
|
|
* plowing), then the moves the unit has still left will be used
|
|
* up. Some work (basically building a road with a hardy pioneer)
|
|
* might actually be finished already in this method-call, in
|
|
* which case the state is set back to UnitState.ACTIVE.
|
|
*
|
|
* @param s The new state for this Unit. Should be one of
|
|
* {UnitState.ACTIVE, UnitState.FORTIFIED, ...}.
|
|
*/
|
|
public void setState(UnitState s) {
|
|
if (state == s) {
|
|
// No need to do anything when the state is unchanged
|
|
return;
|
|
} else if (!checkSetState(s)) {
|
|
throw new IllegalStateException("Illegal UnitState transition: "
|
|
+ state + " -> " + s);
|
|
} else {
|
|
setStateUnchecked(s);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Actually set the unit state.
|
|
*
|
|
* @param s The new {@code UnitState}.
|
|
*/
|
|
protected void setStateUnchecked(UnitState s) {
|
|
// FIXME: move to the server.
|
|
// Cleanup the old UnitState, for example destroy the
|
|
// TileImprovment being built by a pioneer.
|
|
switch (state) {
|
|
case IMPROVING:
|
|
if (workImprovement != null && getWorkLeft() > 0) {
|
|
// Remove the tile improvement if it is incomplete
|
|
// and no one else is working on it
|
|
Tile tile;
|
|
if (!workImprovement.isComplete()
|
|
&& (tile = workImprovement.getTile()) != null
|
|
&& tile.getTileItemContainer() != null
|
|
&& none(tile.getUnits(), u ->
|
|
u != this && u.getState() == UnitState.IMPROVING
|
|
&& u.getWorkImprovement() == workImprovement)) {
|
|
workImprovement.getTile().getTileItemContainer()
|
|
.removeTileItem(workImprovement);
|
|
}
|
|
setWorkImprovement(null);
|
|
}
|
|
break;
|
|
default:
|
|
// do nothing
|
|
break;
|
|
}
|
|
|
|
// Now initiate the new UnitState
|
|
switch (s) {
|
|
case ACTIVE: case FORTIFYING: case SENTRY:
|
|
setWorkLeft(-1);
|
|
break;
|
|
case FORTIFIED:
|
|
setWorkLeft(-1);
|
|
movesLeft = 0;
|
|
break;
|
|
case IMPROVING:
|
|
if (workImprovement == null) {
|
|
setWorkLeft(-1);
|
|
} else {
|
|
setWorkLeft(workImprovement.getTurnsToComplete()
|
|
+ ((getMovesLeft() > 0) ? 0 : 1));
|
|
}
|
|
movesLeft = 0;
|
|
break;
|
|
case SKIPPED: // do nothing
|
|
break;
|
|
default:
|
|
setWorkLeft(-1);
|
|
}
|
|
state = s;
|
|
}
|
|
|
|
/**
|
|
* Sets the given state to all the units that are carried.
|
|
*
|
|
* @param state The {@code UnitState} to set..
|
|
*/
|
|
public void setStateToAllChildren(UnitState state) {
|
|
if (canCarryUnits()) {
|
|
for (Unit u : getUnitList()) u.setState(state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change the owner of this unit.
|
|
*
|
|
* -vis: This routine calls setOwner() and thus has visibility
|
|
* implications. Ideally it should be in ServerUnit but we keep
|
|
* it here for the benefit of the test suite.
|
|
*
|
|
* @param owner The new owner {@code Player}.
|
|
*/
|
|
public void changeOwner(Player owner) {
|
|
final Player oldOwner = this.owner;
|
|
if (oldOwner == owner) return;
|
|
|
|
if (oldOwner == null) {
|
|
logger.warning("Unit " + getId()
|
|
+ " had no owner, when changing owner to " + owner.getId());
|
|
}
|
|
|
|
// Notify the AI before actually changing the owner so the AI
|
|
// can find the owning AIPlayer
|
|
getGame().notifyOwnerChanged(this, oldOwner, owner);
|
|
|
|
// This need to be set right away.
|
|
setOwner(owner);
|
|
|
|
// If its a carrier, we need to update the units it has loaded
|
|
for (Unit u : getUnitList()) u.changeOwner(owner);
|
|
|
|
// Clear education, trade route/orders and home settlement
|
|
if (getTeacher() != null && !canBeStudent(getTeacher())) {
|
|
getTeacher().setStudent(null);
|
|
setTeacher(null);
|
|
}
|
|
if (getTradeRoute() != null) setTradeRoute(null);
|
|
if (getDestination() != null) setDestination(null);
|
|
changeHomeIndianSettlement(null);
|
|
|
|
// Update owner unit lists
|
|
if (oldOwner != null) oldOwner.removeUnit(this);
|
|
if (owner != null) owner.addUnit(this);
|
|
}
|
|
|
|
/**
|
|
* Gets the unit role.
|
|
*
|
|
* @return The {@code Role} of this {@code Unit}.
|
|
*/
|
|
public Role getRole() {
|
|
return role;
|
|
}
|
|
|
|
/**
|
|
* Sets the {@code Role} of this {@code Unit}.
|
|
*
|
|
* @param role The new {@code Role}.
|
|
*/
|
|
public void setRole(Role role) {
|
|
this.role = role;
|
|
}
|
|
|
|
/**
|
|
* Get the role count.
|
|
*
|
|
* @return The current role count.
|
|
*/
|
|
public int getRoleCount() {
|
|
return roleCount;
|
|
}
|
|
|
|
/**
|
|
* Set the role count.
|
|
*
|
|
* @param roleCount The new role count.
|
|
*/
|
|
public void setRoleCount(int roleCount) {
|
|
this.roleCount = roleCount;
|
|
}
|
|
|
|
/**
|
|
* Does this unit have the default role?
|
|
*
|
|
* @return True if the unit has the default {@code Role}.
|
|
*/
|
|
public boolean hasDefaultRole() {
|
|
return role.isDefaultRole();
|
|
}
|
|
|
|
/**
|
|
* Get the last part of the role identifier, which is often used as
|
|
* part of a message key.
|
|
*
|
|
* @return The role suffix.
|
|
*/
|
|
public String getRoleSuffix() {
|
|
return Role.getRoleIdSuffix(role.getId());
|
|
}
|
|
|
|
/**
|
|
* Change the current role of this unit.
|
|
*
|
|
* @param role The new {@code Role}.
|
|
* @param roleCount The new role count.
|
|
*/
|
|
public void changeRole(Role role, int roleCount) {
|
|
if (!role.isCompatibleWith(getRole())) {
|
|
// Clear experience if changing to an incompatible role.
|
|
setExperience(0);
|
|
}
|
|
setRole(role);
|
|
setRoleCount((role.isDefaultRole()) ? 0 : roleCount);
|
|
}
|
|
|
|
/**
|
|
* Change the current role count. On zero, revert to default role.
|
|
*
|
|
* @param delta The change to apply to the role count.
|
|
* @return True if the role count reached zero.
|
|
*/
|
|
public boolean changeRoleCount(int delta) {
|
|
this.roleCount = Math.max(0, this.roleCount + delta);
|
|
if (this.roleCount != 0) return false;
|
|
this.role = getSpecification().getDefaultRole();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Is a role available to this unit?
|
|
*
|
|
* @param role The {@code Role} to test.
|
|
* @return True if the role is available to this unit.
|
|
*/
|
|
public boolean roleIsAvailable(Role role) {
|
|
return role.isAvailableTo(this);
|
|
}
|
|
|
|
/**
|
|
* Filter a list of roles to return only those available to this unit.
|
|
*
|
|
* @param roles The list of {@code Role}s to filter, if null all
|
|
* available roles are used.
|
|
* @return A list of available {@code Role}s.
|
|
*/
|
|
public List<Role> getAvailableRolesList(List<Role> roles) {
|
|
if (roles == null) roles = getSpecification().getRolesList();
|
|
return transform(roles, r -> roleIsAvailable(r));
|
|
}
|
|
|
|
/**
|
|
* Filter a list of roles to return only those available to this unit,
|
|
* returning a stream.
|
|
*
|
|
* @param roles The list of {@code Role}s to filter, if null all
|
|
* available roles are used.
|
|
* @return A stream of available {@code Role}s.
|
|
*/
|
|
public Stream<Role> getAvailableRoles(List<Role> roles) {
|
|
return getAvailableRolesList(roles).stream();
|
|
}
|
|
|
|
/**
|
|
* Get a military role for this unit.
|
|
*
|
|
* @return A military {@code Role}, or null if none found.
|
|
*/
|
|
public Role getMilitaryRole() {
|
|
final Role bestMilitaryRole = getSpecification().getMilitaryRoles()
|
|
.filter(r -> roleIsAvailable(r) && !r.hasAbility(Ability.SPEAK_WITH_CHIEF))
|
|
.sorted((a, b) -> Double.compare(b.getOffence(), a.getOffence()))
|
|
.findFirst().orElse(null);
|
|
|
|
return bestMilitaryRole;
|
|
}
|
|
|
|
/**
|
|
* Gets the military roles for this unit that is not a scout.
|
|
*
|
|
* @return A sorted list of military roles for this unit, with
|
|
* the best roles first.
|
|
*/
|
|
public List<Role> getSortedMilitaryRoles() {
|
|
return getSpecification().getMilitaryRoles()
|
|
.filter(r -> roleIsAvailable(r) && !r.hasAbility(Ability.SPEAK_WITH_CHIEF))
|
|
.sorted((a, b) -> Double.compare(b.getOffence(), a.getOffence()))
|
|
.collect(Collectors.toList());
|
|
}
|
|
|
|
/**
|
|
* Get the change in goods required to change to a new role/count.
|
|
*
|
|
* @param role The new {@code Role} to change to.
|
|
* @param roleCount The new role count.
|
|
* @return A list of {@code AbstractGoods} defining the change
|
|
* in goods required.
|
|
*/
|
|
public List<AbstractGoods> getGoodsDifference(Role role, int roleCount) {
|
|
return Role.getGoodsDifference(getRole(), getRoleCount(),
|
|
role, roleCount);
|
|
}
|
|
|
|
/**
|
|
* Sets the units location without updating any other variables
|
|
*
|
|
* get/setLocation are in Locatable interface.
|
|
*
|
|
* -vis: This routine changes player visibility.
|
|
*
|
|
* @param newLocation The new {@code Location}.
|
|
*/
|
|
public void setLocationNoUpdate(Location newLocation) {
|
|
this.location = newLocation;
|
|
}
|
|
|
|
/**
|
|
* Verifies if the unit is aboard a carrier
|
|
*
|
|
* @return True if the unit is aboard a carrier.
|
|
*/
|
|
public boolean isOnCarrier() {
|
|
return getLocation() instanceof Unit;
|
|
}
|
|
|
|
/**
|
|
* Is this unit on a tile?
|
|
*
|
|
* @return True if the unit is on a tile.
|
|
*/
|
|
public boolean isOnTile() {
|
|
return getLocation() instanceof Tile;
|
|
}
|
|
|
|
/**
|
|
* Gets the carrier this unit is aboard if any.
|
|
*
|
|
* @return The carrier this unit is aboard, or null if none.
|
|
*/
|
|
public Unit getCarrier() {
|
|
return (isOnCarrier()) ? ((Unit)getLocation()) : null;
|
|
}
|
|
|
|
/**
|
|
* Checks whether this {@code Unit} is at sea off the map, or
|
|
* on board of a carrier that is.
|
|
*
|
|
* @return True if at sea.
|
|
*/
|
|
public boolean isAtSea() {
|
|
return (isOnCarrier()) ? getCarrier().isAtSea()
|
|
: getLocation() instanceof HighSeas;
|
|
}
|
|
|
|
/**
|
|
* Checks if this unit is running a mission.
|
|
*
|
|
* @return True if this unit is running a mission.
|
|
*/
|
|
public boolean isInMission() {
|
|
return hasAbility(Ability.ESTABLISH_MISSION)
|
|
&& getLocation() instanceof IndianSettlement;
|
|
}
|
|
|
|
/**
|
|
* Checks whether this unit is working inside a colony.
|
|
*
|
|
* @return True if in colony.
|
|
*/
|
|
public boolean isInColony() {
|
|
return getLocation() instanceof WorkLocation;
|
|
}
|
|
|
|
/**
|
|
* Is this unit on a tile?
|
|
*
|
|
* @return True if this unit is on a tile.
|
|
*/
|
|
public boolean hasTile() {
|
|
return getTile() != null;
|
|
}
|
|
|
|
/**
|
|
* Gets the work location this unit is working in.
|
|
*
|
|
* @return The current {@code WorkLocation}, or null if none.
|
|
*/
|
|
public WorkLocation getWorkLocation() {
|
|
return (isInColony()) ? (WorkLocation)getLocation() : null;
|
|
}
|
|
|
|
/**
|
|
* Get the work tile this unit is working in, if any.
|
|
*
|
|
* @return The current work {@code Tile}, if any.
|
|
*/
|
|
public Tile getWorkTile() {
|
|
return (getLocation() instanceof WorkLocation)
|
|
? ((WorkLocation)getLocation()).getWorkTile()
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Gets the entry location for this unit to use when returning from
|
|
* {@link Europe}.
|
|
*
|
|
* @return The entry {@code Location}.
|
|
*/
|
|
public Location getEntryLocation() {
|
|
return this.entryLocation;
|
|
}
|
|
|
|
/**
|
|
* Sets the entry location in which this unit will be put when
|
|
* returning from {@link Europe}.
|
|
*
|
|
* @param entryLocation The new entry {@code Location}.
|
|
* @see #getEntryLocation
|
|
*/
|
|
public void setEntryLocation(Location entryLocation) {
|
|
this.entryLocation = entryLocation;
|
|
if (this.entryLocation != null) {
|
|
Tile tile = this.entryLocation.getTile();
|
|
if (tile != null) owner.setEntryTile(tile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the entry tile for this unit, or if null the default
|
|
* entry location for the owning player.
|
|
*
|
|
* @return The entry {@code Tile}.
|
|
*/
|
|
public Tile getFullEntryLocation() {
|
|
return (this.entryLocation != null) ? this.entryLocation.getTile()
|
|
: owner.getEntryTile();
|
|
}
|
|
|
|
/**
|
|
* Get the moves left this turn.
|
|
*
|
|
* @return The number of moves this {@code Unit} has left.
|
|
*/
|
|
@Override
|
|
public int getMovesLeft() {
|
|
return movesLeft;
|
|
}
|
|
|
|
/**
|
|
* Get a carried unit by identifier.
|
|
*
|
|
* @param id The identifier of the carried unit.
|
|
* @return The {@code Unit} found, or null if not present.
|
|
*/
|
|
public Unit getCarriedUnitById(String id) {
|
|
if (id == null) return null;
|
|
for (Unit u : getUnitList()) {
|
|
if (id.equals(u.getId())) return u;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Sets the moves left this turn.
|
|
*
|
|
* @param moves The new amount of moves left this {@code Unit}
|
|
* should have.
|
|
*/
|
|
public void setMovesLeft(int moves) {
|
|
this.movesLeft = (moves < 0) ? 0 : moves;
|
|
}
|
|
|
|
/**
|
|
* Gets the type of goods this unit is producing in its current occupation.
|
|
*
|
|
* @return The type of goods this unit is producing.
|
|
*/
|
|
public GoodsType getWorkType() {
|
|
return workType;
|
|
}
|
|
|
|
/**
|
|
* Set the type of goods this unit is producing in its current
|
|
* occupation.
|
|
*
|
|
* @param type The {@code GoodsType} to produce.
|
|
*/
|
|
public void setWorkType(GoodsType type) {
|
|
this.workType = type;
|
|
}
|
|
|
|
/**
|
|
* Change the type of goods this unit is producing in its current
|
|
* occupation. Updates the work location production and the unit
|
|
* experience type if necessary.
|
|
*
|
|
* @param type The {@code GoodsType} to produce.
|
|
*/
|
|
public void changeWorkType(GoodsType type) {
|
|
setWorkType(type);
|
|
WorkLocation wl = getWorkLocation();
|
|
if (wl != null) wl.updateProductionType();
|
|
}
|
|
|
|
/**
|
|
* Gets the type of goods this unit has accrued experience producing.
|
|
*
|
|
* @return The {@code GoodsType} this unit would produce.
|
|
*/
|
|
public GoodsType getExperienceType() {
|
|
return experienceType;
|
|
}
|
|
|
|
/**
|
|
* Sets the type of goods this unit has accrued experience producing.
|
|
*
|
|
* @param type The {@code GoodsType} this unit would produce.
|
|
*/
|
|
public void changeExperienceType(GoodsType type) {
|
|
if (experienceType != type) {
|
|
experience = 0;
|
|
experienceType = type;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the experience of this {@code Unit} at its current
|
|
* experienceType.
|
|
*
|
|
* @return The experience of this {@code Unit} at its current
|
|
* experienceType.
|
|
* @see #modifyExperience
|
|
*/
|
|
public int getExperience() {
|
|
return experience;
|
|
}
|
|
|
|
/**
|
|
* Sets the experience of this {@code Unit} at its current
|
|
* experienceType.
|
|
*
|
|
* @param experience The new experience of this {@code Unit}
|
|
* at its current experienceType.
|
|
* @see #modifyExperience
|
|
*/
|
|
public void setExperience(int experience) {
|
|
this.experience = Math.min(experience,
|
|
getType().getMaximumExperience());
|
|
}
|
|
|
|
/**
|
|
* Modifies the experience of this {@code Unit} at its current
|
|
* experienceType.
|
|
*
|
|
* @param value The value by which to modify the experience of this
|
|
* {@code Unit}.
|
|
* @see #getExperience
|
|
*/
|
|
public void modifyExperience(int value) {
|
|
experience += value;
|
|
}
|
|
|
|
/**
|
|
* Gets the amount of work left.
|
|
*
|
|
* @return The amount of work left.
|
|
*/
|
|
public int getWorkLeft() {
|
|
return workLeft;
|
|
}
|
|
|
|
/**
|
|
* Sets the amount of work left.
|
|
*
|
|
* @param workLeft The new amount of work left.
|
|
*/
|
|
public void setWorkLeft(int workLeft) {
|
|
this.workLeft = workLeft;
|
|
}
|
|
|
|
/**
|
|
* Get the number of turns of work left.
|
|
*
|
|
* @return The number of turns of work left.
|
|
*/
|
|
public int getWorkTurnsLeft() {
|
|
return (state == UnitState.IMPROVING
|
|
&& this.type.hasAbility(Ability.EXPERT_PIONEER))
|
|
? (getWorkLeft() + 1) / 2
|
|
: getWorkLeft();
|
|
}
|
|
|
|
/**
|
|
* Gets the TileImprovement that this pioneer is contributing to.
|
|
*
|
|
* @return The {@code TileImprovement} the pioneer is working on.
|
|
*/
|
|
public TileImprovement getWorkImprovement() {
|
|
return workImprovement;
|
|
}
|
|
|
|
/**
|
|
* Sets the TileImprovement that this pioneer is contributing to.
|
|
*
|
|
* @param imp The new {@code TileImprovement} the pioneer is to
|
|
* work on.
|
|
*/
|
|
public void setWorkImprovement(TileImprovement imp) {
|
|
workImprovement = imp;
|
|
}
|
|
|
|
/**
|
|
* Get the unit being taught.
|
|
*
|
|
* @return A student {@code Unit} if any.
|
|
*/
|
|
public final Unit getStudent() {
|
|
return student;
|
|
}
|
|
|
|
/**
|
|
* Set the student unit.
|
|
*
|
|
* @param newStudent The new student {@code Unit}.
|
|
*/
|
|
public final void setStudent(final Unit newStudent) {
|
|
Unit oldStudent = this.student;
|
|
if (oldStudent == newStudent) return;
|
|
|
|
if (newStudent == null) {
|
|
this.student = null;
|
|
if (oldStudent != null && oldStudent.getTeacher() == this) {
|
|
oldStudent.setTeacher(null);
|
|
}
|
|
} else if (newStudent.getColony() != null
|
|
&& newStudent.getColony() == getColony()
|
|
&& newStudent.canBeStudent(this)) {
|
|
if (oldStudent != null && oldStudent.getTeacher() == this) {
|
|
oldStudent.setTeacher(null);
|
|
}
|
|
this.student = newStudent;
|
|
newStudent.setTeacher(this);
|
|
} else {
|
|
throw new IllegalStateException("Unit can not be student: "
|
|
+ newStudent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the unit teaching this one.
|
|
*
|
|
* @return A teacher {@code Unit}.
|
|
*/
|
|
public final Unit getTeacher() {
|
|
return teacher;
|
|
}
|
|
|
|
/**
|
|
* Set the teacher for this unit.
|
|
*
|
|
* @param newTeacher The new teacher {@code Unit}.
|
|
*/
|
|
public final void setTeacher(final Unit newTeacher) {
|
|
Unit oldTeacher = this.teacher;
|
|
if (newTeacher == oldTeacher) return;
|
|
|
|
if (newTeacher == null) {
|
|
this.teacher = null;
|
|
if (oldTeacher != null && oldTeacher.getStudent() == this) {
|
|
oldTeacher.setStudent(null);
|
|
}
|
|
} else {
|
|
UnitType skillTaught = newTeacher.getType().getSkillTaught();
|
|
if (newTeacher.getColony() != null
|
|
&& newTeacher.getColony() == getColony()
|
|
&& getColony().canTrain(skillTaught)) {
|
|
if (oldTeacher != null && oldTeacher.getStudent() == this) {
|
|
oldTeacher.setStudent(null);
|
|
}
|
|
this.teacher = newTeacher;
|
|
this.teacher.setStudent(this);
|
|
} else {
|
|
throw new IllegalStateException("Unit can not be teacher: "
|
|
+ newTeacher);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns this unit has been training.
|
|
*
|
|
* @return The number of turns of training this {@code Unit} has
|
|
* given.
|
|
* @see #setTurnsOfTraining
|
|
* @see #getNeededTurnsOfTraining
|
|
*/
|
|
public int getTurnsOfTraining() {
|
|
return turnsOfTraining;
|
|
}
|
|
|
|
/**
|
|
* Sets the number of turns this unit has been training.
|
|
*
|
|
* @param turnsOfTraining The number of turns of training this
|
|
* {@code Unit} has given.
|
|
* @see #getNeededTurnsOfTraining
|
|
*/
|
|
public void setTurnsOfTraining(int turnsOfTraining) {
|
|
this.turnsOfTraining = turnsOfTraining;
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns this unit has to train to educate a student.
|
|
* This value is only meaningful for units that can be put in a school.
|
|
*
|
|
* @return The turns of training needed to teach its current type
|
|
* to a free colonist or to promote an indentured servant or a
|
|
* petty criminal.
|
|
* @see #getTurnsOfTraining
|
|
*/
|
|
public int getNeededTurnsOfTraining() {
|
|
// number of turns is 4/6/8 for skill 1/2/3
|
|
int result = 0;
|
|
if (student != null) {
|
|
result = getSpecification()
|
|
.getNeededTurnsOfTraining(getType(), student.getType());
|
|
if (getColony() != null) {
|
|
result -= getColony().getProductionBonus();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get a unit change for this unit.
|
|
*
|
|
* @param change The identifier for the required change type.
|
|
* @return The {@code UnitChange} found, or null if the
|
|
* change is impossible.
|
|
*/
|
|
public UnitTypeChange getUnitChange(String change) {
|
|
return getUnitChange(change, null);
|
|
}
|
|
|
|
/**
|
|
* Get a unit change for this unit.
|
|
*
|
|
* @param change The identifier for the required change type.
|
|
* @param toType A {@code UnitType} to change to.
|
|
* @return The {@code UnitChange} found, or null if the
|
|
* change is impossible.
|
|
*/
|
|
public UnitTypeChange getUnitChange(String change, UnitType toType) {
|
|
UnitChangeType uct = getSpecification().getUnitChangeType(change);
|
|
if (uct != null && uct.getOwnerChange()) {
|
|
throw new RuntimeException("2-arg getUnitChange of " + this
|
|
+ " change=" + change + " which changes owner");
|
|
}
|
|
return getUnitChange(change, toType, getOwner());
|
|
}
|
|
|
|
/**
|
|
* Get a unit change for this unit, including the ownership check.
|
|
*
|
|
* @param change The identifier for the required change type.
|
|
* @param toType A {@code UnitType} to change to.
|
|
* @param player The expected {@code Player} that will own the unit.
|
|
* @return The {@code UnitChange} found, or null if the
|
|
* change is impossible.
|
|
*/
|
|
public UnitTypeChange getUnitChange(String change, UnitType toType,
|
|
Player player) {
|
|
if (player == null) {
|
|
throw new RuntimeException("getUnitChange null player: " + change);
|
|
}
|
|
UnitChangeType uct = getSpecification().getUnitChangeType(change);
|
|
if (uct != null && uct.getOwnerChange() != (player != getOwner())) {
|
|
throw new RuntimeException("getUnitChange of " + this
|
|
+ " change=" + change
|
|
+ " getOwnerChange=" + uct.getOwnerChange()
|
|
+ " != player-change=" + (player != getOwner())
|
|
+ " player=" + player.getSuffix()
|
|
+ " owner=" + getOwner().getSuffix());
|
|
}
|
|
UnitTypeChange uc = (uct == null || !uct.appliesTo(this)) ? null
|
|
: uct.getUnitChange(getType(), toType);
|
|
return (uc == null || !uc.isAvailableTo(player) || !uc.appliesTo(this)) ? null : uc;
|
|
}
|
|
|
|
/**
|
|
* Get the skill another unit type can teach this unit.
|
|
*
|
|
* Public for the test suite.
|
|
*
|
|
* @param teacherType The {@code UnitType} to teach this unit.
|
|
* @return The {@code UnitType} (skill) this unit can learn.
|
|
*/
|
|
public UnitType getTeachingType(UnitType teacherType) {
|
|
UnitType ret = (getSpecification()
|
|
.getUnitChangeType(UnitChangeType.EDUCATION).appliesTo(this))
|
|
? getType().getTeachingType(teacherType)
|
|
: null;
|
|
return (ret == null || !ret.isAvailableTo(getOwner())) ? null : ret;
|
|
}
|
|
|
|
/**
|
|
* Get the skill another unit can teach this unit.
|
|
*
|
|
* @param teacher The {@code Unit} to teach this unit.
|
|
* @return The {@code UnitType} (skill) this unit can learn.
|
|
*/
|
|
public UnitType getTeachingType(Unit teacher) {
|
|
return getTeachingType(teacher.getType());
|
|
}
|
|
|
|
/**
|
|
* Can this unit be a student of a teacher unit?
|
|
*
|
|
* @param teacher The teacher {@code Unit} which is trying to
|
|
* teach it.
|
|
* @return True if the unit can be taught by the teacher.
|
|
*/
|
|
public boolean canBeStudent(Unit teacher) {
|
|
return teacher != null && teacher != this
|
|
&& getTeachingType(teacher) != null;
|
|
}
|
|
|
|
/**
|
|
* Gets the nationality of this Unit.
|
|
*
|
|
* Nationality represents a Unit's personal allegiance to a
|
|
* nation. This may conflict with who currently issues orders to
|
|
* the Unit (the owner).
|
|
*
|
|
* @return The nationality of this Unit.
|
|
*/
|
|
public final String getNationality() {
|
|
return this.nationality;
|
|
}
|
|
|
|
/**
|
|
* Sets the nationality of this Unit. A unit will change
|
|
* nationality when it switches owners willingly. Currently only
|
|
* Converts do this, but it opens the possibility of
|
|
* naturalisation.
|
|
*
|
|
* @param newNationality The new nationality of this Unit.
|
|
*/
|
|
public final void setNationality(String newNationality) {
|
|
this.nationality = newNationality;
|
|
}
|
|
|
|
/**
|
|
* Gets the ethnicity of this Unit.
|
|
*
|
|
* Ethnicity is inherited from the inhabitants of the place where
|
|
* the Unit was born. Allows former converts to become
|
|
* native-looking colonists.
|
|
*
|
|
* @return The ethnicity of this Unit.
|
|
*/
|
|
public final String getEthnicity() {
|
|
return this.ethnicity;
|
|
}
|
|
|
|
/**
|
|
* Sets the ethnicity of this Unit.
|
|
*
|
|
* @param newEthnicity The new ethnicity of this Unit.
|
|
*/
|
|
public final void setEthnicity(String newEthnicity) {
|
|
this.ethnicity = newEthnicity;
|
|
}
|
|
|
|
/**
|
|
* Identifies whether this unit came from a native tribe.
|
|
*
|
|
* @return Whether this unit looks native or not.
|
|
*/
|
|
public boolean hasNativeEthnicity() {
|
|
try {
|
|
// FIXME: getNation() could fail, but getNationType()
|
|
// doesn't work as expected
|
|
return getGame().getSpecification().getNation(ethnicity)
|
|
.getType().isIndian();
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the {@code IndianSettlement} home for this unit.
|
|
*
|
|
* @return The home {@code IndianSettlement} of this unit.
|
|
*/
|
|
public IndianSettlement getHomeIndianSettlement() {
|
|
return indianSettlement;
|
|
}
|
|
|
|
/**
|
|
* Sets the home {@code IndianSettlement} for this unit.
|
|
*
|
|
* @param indianSettlement The {@code IndianSettlement} that this unit
|
|
* considers to be its home.
|
|
*/
|
|
public void setHomeIndianSettlement(IndianSettlement indianSettlement) {
|
|
this.indianSettlement = indianSettlement;
|
|
}
|
|
|
|
/**
|
|
* Changes the home {@code IndianSettlement} for this unit.
|
|
*
|
|
* @param indianSettlement The {@code IndianSettlement} that should
|
|
* now own this {@code Unit} and be considered this unit's home.
|
|
* @return The old {@code IndianSettlement}.
|
|
*/
|
|
public IndianSettlement changeHomeIndianSettlement(IndianSettlement indianSettlement) {
|
|
if (this.indianSettlement != null) {
|
|
this.indianSettlement.removeOwnedUnit(this);
|
|
}
|
|
|
|
IndianSettlement ret = this.indianSettlement;
|
|
this.indianSettlement = indianSettlement;
|
|
|
|
if (indianSettlement != null) {
|
|
indianSettlement.addOwnedUnit(this);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets the unit hit points.
|
|
*
|
|
* This is currently only used for damaged ships, but might get an
|
|
* extended use later.
|
|
*
|
|
* @return The hit points this {@code Unit} has.
|
|
* @see UnitType#getHitPoints
|
|
*/
|
|
public int getHitPoints() {
|
|
return hitPoints;
|
|
}
|
|
|
|
/**
|
|
* Gets the maximum hitspoints for the unit.
|
|
*
|
|
* @return The hit points this {@code Unit} has at full health.
|
|
* @see UnitType#getHitPoints
|
|
*/
|
|
public int getMaximumHitPoints() {
|
|
return type.getHitPoints();
|
|
}
|
|
|
|
/**
|
|
* Sets the hit points for this unit.
|
|
*
|
|
* @param hitPoints The new hit points for this unit.
|
|
*/
|
|
public void setHitPoints(int hitPoints) {
|
|
this.hitPoints = hitPoints;
|
|
}
|
|
|
|
/**
|
|
* Checks if this unit is under repair.
|
|
*
|
|
* @return True if under repair.
|
|
*/
|
|
public boolean isDamagedAndUnderForcedRepair() {
|
|
return isDamaged() && !getSpecification().hasAbility(Ability.HITPOINTS_COMBAT_MODEL);
|
|
}
|
|
|
|
public boolean isDamaged() {
|
|
return hitPoints < getMaximumHitPoints();
|
|
}
|
|
|
|
/**
|
|
* Get how many turns left to be repaired
|
|
*
|
|
* @return The number of turns left to be repaired.
|
|
*/
|
|
public int getTurnsForRepair() {
|
|
return getMaximumHitPoints() - getHitPoints();
|
|
}
|
|
|
|
/**
|
|
* Get the destination of this unit.
|
|
*
|
|
* @return The destination {@code Location} of this {@code Unit}.
|
|
*/
|
|
public Location getDestination() {
|
|
return destination;
|
|
}
|
|
|
|
/**
|
|
* Sets the destination of this unit.
|
|
*
|
|
* @param newDestination The new destination {@code Location}.
|
|
*/
|
|
public void setDestination(Location newDestination) {
|
|
this.destination = newDestination;
|
|
}
|
|
|
|
/**
|
|
* Get the unit trade route, if any.
|
|
*
|
|
* @return The {@code TradeRoute}, or null if none.
|
|
*/
|
|
public final TradeRoute getTradeRoute() {
|
|
return tradeRoute;
|
|
}
|
|
|
|
/**
|
|
* Set the unit trade route.
|
|
*
|
|
* @param newTradeRoute The new {@code TradeRoute} value.
|
|
*/
|
|
public final void setTradeRoute(final TradeRoute newTradeRoute) {
|
|
this.tradeRoute = newTradeRoute;
|
|
}
|
|
|
|
/**
|
|
* Get the stop the unit is heading for or at.
|
|
*
|
|
* @return The target {@code TradeRouteStop}.
|
|
*/
|
|
public TradeRouteStop getStop() {
|
|
return (validateCurrentStop() < 0) ? null
|
|
: getTradeRoute().getStop(currentStop);
|
|
}
|
|
|
|
/**
|
|
* Get the stop the unit is heading for or at.
|
|
*
|
|
* @return The target {@code TradeRouteStop}.
|
|
*/
|
|
public List<TradeRouteStop> getCurrentStops() {
|
|
if (validateCurrentStop() < 0) return null;
|
|
List<TradeRouteStop> stops = getTradeRoute().getStopList();
|
|
rotate(stops, currentStop);
|
|
return stops;
|
|
}
|
|
|
|
/**
|
|
* Get the current trade route stop.
|
|
*
|
|
* @return The current stop index.
|
|
*/
|
|
public int getCurrentStop() {
|
|
return currentStop;
|
|
}
|
|
|
|
/**
|
|
* Set the current stop.
|
|
*
|
|
* @param currentStop A new value for the currentStop.
|
|
*/
|
|
public void setCurrentStop(int currentStop) {
|
|
this.currentStop = currentStop;
|
|
}
|
|
|
|
/**
|
|
* Validate and return the current stop.
|
|
*
|
|
* @return The current stop index, or negative on failure.
|
|
*/
|
|
private int validateCurrentStop() {
|
|
if (tradeRoute == null) {
|
|
currentStop = -1;
|
|
} else {
|
|
int stopCount = tradeRoute.getStopCount();
|
|
if (stopCount <= 0) {
|
|
currentStop = -1;
|
|
} else if (currentStop < 0 || currentStop >= stopCount) {
|
|
// The current stop can become out of range if the trade
|
|
// route is modified.
|
|
currentStop = 0;
|
|
}
|
|
}
|
|
return currentStop;
|
|
}
|
|
|
|
/**
|
|
* Convenience function to check if a unit is at a stop.
|
|
*
|
|
* @param stop The {@code TradeRouteStop} to check.
|
|
* @return True if the unit is at the given stop.
|
|
*/
|
|
public boolean atStop(TradeRouteStop stop) {
|
|
return Map.isSameLocation(getLocation(), stop.getLocation());
|
|
}
|
|
|
|
/**
|
|
* Get the current trade location.
|
|
*
|
|
* @return The {@code TradeLocation} for this unit.
|
|
*/
|
|
public TradeLocation getTradeLocation() {
|
|
Colony colony;
|
|
IndianSettlement is;
|
|
return ((colony = getColony()) != null) ? colony
|
|
: ((is = getIndianSettlement()) != null) ? is
|
|
: (isInEurope()) ? (TradeLocation)getOwner().getEurope()
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Get the current amount of treasure in this unit.
|
|
*
|
|
* @return The amount of treasure.
|
|
* @exception IllegalStateException if this is not a treasure
|
|
* carrying unit.
|
|
*/
|
|
public int getTreasureAmount() {
|
|
return this.treasureAmount;
|
|
}
|
|
|
|
/**
|
|
* Set the amount of treasure in this unit.
|
|
*
|
|
* @param amount The new amount of treasure.
|
|
*/
|
|
public void setTreasureAmount(int amount) {
|
|
this.treasureAmount = amount;
|
|
}
|
|
|
|
/**
|
|
* Gets the attrition of this unit.
|
|
*
|
|
* @return The attrition of this unit.
|
|
*/
|
|
public int getAttrition() {
|
|
return attrition;
|
|
}
|
|
|
|
/**
|
|
* Sets the attrition of this unit.
|
|
*
|
|
* @param attrition The new attrition of this unit.
|
|
*/
|
|
public void setAttrition(int attrition) {
|
|
this.attrition = attrition;
|
|
}
|
|
|
|
/**
|
|
* Get the visible amount of goods that is carried by this unit.
|
|
*
|
|
* @return The visible amount of goods carried by this {@code Unit}.
|
|
*/
|
|
public int getVisibleGoodsCount() {
|
|
return (visibleGoodsCount >= 0) ? visibleGoodsCount
|
|
: getGoodsSpaceTaken();
|
|
}
|
|
|
|
|
|
// Combat routines
|
|
|
|
/**
|
|
* Gets a role that can be equipped automatically assumed
|
|
* in case of an attack.
|
|
*
|
|
* Paul Revere makes an unarmed colonist in a settlement pick up a
|
|
* stock-piled musket if attacked, so the bonus should be applied
|
|
* for unarmed colonists inside colonies where there are muskets
|
|
* available. Natives can also auto-arm.
|
|
*
|
|
* @return A {@code Role} that can be automatically assumed
|
|
* by this unit, or null if none.
|
|
*/
|
|
public Role getAutomaticRole() {
|
|
if (!hasDefaultRole()) return null;
|
|
Settlement settlement = (isInColony()) ? getColony()
|
|
: (getLocation() instanceof IndianSettlement)
|
|
? (Settlement)getLocation()
|
|
: null;
|
|
if (settlement == null) return null;
|
|
|
|
final Specification spec = getSpecification();
|
|
return find(transform(flatten(getAbilities(Ability.AUTOMATIC_EQUIPMENT),
|
|
Ability::getScopes),
|
|
alwaysTrue(), s -> spec.getRole(s.getType())),
|
|
r -> r != null
|
|
&& settlement.containsGoods(getGoodsDifference(r, 1)));
|
|
}
|
|
|
|
/**
|
|
* After winning a battle, can this unit capture the loser's role
|
|
* equipment?
|
|
*
|
|
* @param role The loser unit {@code Role}.
|
|
* @return The {@code Role} available to this unit as a result
|
|
* of capturing the loser equipment.
|
|
*/
|
|
public Role canCaptureEquipment(Role role) {
|
|
if (!hasAbility(Ability.CAPTURE_EQUIPMENT)) return null;
|
|
final Specification spec = getSpecification();
|
|
final Role oldRole = getRole();
|
|
return find(getAvailableRoles(spec.getMilitaryRolesList()),
|
|
r -> any(r.getRoleChanges(), rc ->
|
|
rc.getFrom(spec) == oldRole && rc.getCapture(spec) == role));
|
|
}
|
|
|
|
/**
|
|
* Does losing a piece of equipment mean the death of this unit?
|
|
*
|
|
* @return True if the unit is doomed.
|
|
*/
|
|
public boolean losingEquipmentKillsUnit() {
|
|
return hasAbility(Ability.DISPOSE_ON_ALL_EQUIPMENT_LOST)
|
|
&& getRole().getDowngrade() == null;
|
|
}
|
|
|
|
/**
|
|
* Does losing equipment mean the demotion of this unit?
|
|
*
|
|
* @return True if the unit is to be demoted.
|
|
*/
|
|
public boolean losingEquipmentDemotesUnit() {
|
|
return hasAbility(Ability.DEMOTE_ON_ALL_EQUIPMENT_LOST)
|
|
&& getRole().getDowngrade() == null;
|
|
}
|
|
|
|
/**
|
|
* Does the unit have arms?
|
|
*
|
|
* @return True if the unit has arms.
|
|
*/
|
|
public boolean isArmed() {
|
|
return hasAbility(Ability.ARMED);
|
|
}
|
|
|
|
/**
|
|
* Does the unit have a mount?
|
|
*
|
|
* @return True if the unit have a mount.
|
|
*/
|
|
public boolean isMounted() {
|
|
return hasAbility(Ability.MOUNTED);
|
|
}
|
|
|
|
/**
|
|
* Is the unit a beached ship?
|
|
*
|
|
* @return True if the unit is a beached ship.
|
|
*/
|
|
public boolean isBeached() {
|
|
return isBeached(getTile());
|
|
}
|
|
|
|
/**
|
|
* Would this unit be beached if it was on a particular tile?
|
|
*
|
|
* @param tile The {@code Tile} to check.
|
|
* @return True if the unit is a beached ship.
|
|
*/
|
|
public boolean isBeached(Tile tile) {
|
|
return tile != null && tile.isLand() && !tile.hasSettlement()
|
|
&& isNaval();
|
|
}
|
|
|
|
/**
|
|
* Checks if this is an defensive unit. That is: a unit which can
|
|
* be used to defend a {@code Settlement}.
|
|
*
|
|
* Note! As this method is used by the AI it really means that the
|
|
* unit can defend as is. To be specific an unarmed colonist is
|
|
* not defensive yet, even if Paul Revere and stockpiled muskets
|
|
* are available. That check is only performed on an actual
|
|
* attack.
|
|
*
|
|
* A settlement is lost when there are no more defensive units.
|
|
*
|
|
* @return True if this is a defensive unit meaning it can be used
|
|
* to defend a {@code Colony}. This would normally mean
|
|
* that a defensive unit also will be offensive.
|
|
*/
|
|
public boolean isDefensiveUnit() {
|
|
return (this.type.isDefensive() || getRole().isDefensive())
|
|
&& !isCarrier(); // Not wagons or ships
|
|
}
|
|
|
|
/**
|
|
* Checks if this is an offensive unit. That is, one that can
|
|
* attack other units.
|
|
*
|
|
* @return True if this is an offensive unit.
|
|
*/
|
|
public boolean isOffensiveUnit() {
|
|
return this.type.isOffensive() || getRole().isOffensive();
|
|
}
|
|
|
|
/**
|
|
* Can this unit ambush another?
|
|
*
|
|
* @param defender The defending {@code Unit}.
|
|
* @return True if an ambush attack is possible.
|
|
*/
|
|
public boolean canAmbush(Unit defender) {
|
|
return isOnTile() && getSettlement() == null
|
|
&& defender.isOnTile() && defender.getSettlement() == null
|
|
&& defender.getState() != UnitState.FORTIFIED
|
|
&& (hasAbility(Ability.AMBUSH_BONUS)
|
|
|| defender.hasAbility(Ability.AMBUSH_PENALTY))
|
|
&& (getTile().hasAbility(Ability.AMBUSH_TERRAIN)
|
|
|| defender.getTile().hasAbility(Ability.AMBUSH_TERRAIN));
|
|
}
|
|
|
|
/**
|
|
* Is an alternate unit a better defender than the current choice.
|
|
* Prefer if there is no current defender, or if the alternate
|
|
* unit is better armed, or provides greater defensive power and
|
|
* does not replace a defensive unit defender with a non-defensive
|
|
* unit.
|
|
*
|
|
* @param defender The current defender {@code Unit}.
|
|
* @param defenderPower Its defence power.
|
|
* @param other An alternate {@code Unit}.
|
|
* @param otherPower Its defence power.
|
|
* @return True if the other unit should be preferred.
|
|
*/
|
|
public static boolean betterDefender(Unit defender, double defenderPower,
|
|
Unit other, double otherPower) {
|
|
if (defender == null) {
|
|
return true;
|
|
} else if (defender.isPerson() && other.isPerson()
|
|
&& !defender.isArmed() && other.isArmed()) {
|
|
return true;
|
|
} else if (defender.isPerson() && other.isPerson()
|
|
&& defender.isArmed() && !other.isArmed()) {
|
|
return false;
|
|
} else if (!defender.isDefensiveUnit() && other.isDefensiveUnit()) {
|
|
return true;
|
|
} else if (defender.isDefensiveUnit() && !other.isDefensiveUnit()) {
|
|
return false;
|
|
} else {
|
|
return defenderPower < otherPower;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Finds the closest {@code Location} to this tile where
|
|
* this ship can be repaired, excluding the current colony.
|
|
*
|
|
* @return The closest {@code Location} where a ship can be
|
|
* repaired.
|
|
*/
|
|
public Location getRepairLocation() {
|
|
final Player player = getOwner();
|
|
final Colony notHere = getTile().getColony();
|
|
final Predicate<Colony> repairPred = c ->
|
|
c != notHere && c.hasAbility(Ability.REPAIR_UNITS);
|
|
Location loc = getClosestColony(transform(player.getColonies(), repairPred));
|
|
return (loc != null) ? loc : player.getEurope();
|
|
}
|
|
|
|
/**
|
|
* Damage this unit (which should be a ship).
|
|
*
|
|
* @param repair A {@code Location} to send the ship to for repair.
|
|
*/
|
|
public void damageShip(Location repair) {
|
|
setHitPoints(1);
|
|
setDestination(null);
|
|
setLocation(repair);//-vis(player)
|
|
setState(Unit.UnitState.ACTIVE);
|
|
setMovesLeft(0);
|
|
}
|
|
|
|
|
|
// Movement handling
|
|
|
|
/**
|
|
* A move type.
|
|
*
|
|
* @see Unit#getMoveType(Direction)
|
|
*/
|
|
public enum MoveType {
|
|
MOVE(null, true),
|
|
MOVE_HIGH_SEAS(null, true),
|
|
EXPLORE_LOST_CITY_RUMOUR(null, true),
|
|
ATTACK_UNIT(null, false),
|
|
ATTACK_SETTLEMENT(null, false),
|
|
EMBARK(null, false),
|
|
ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST(null, false),
|
|
ENTER_INDIAN_SETTLEMENT_WITH_SCOUT(null, false),
|
|
ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY(null, false),
|
|
ENTER_FOREIGN_COLONY_WITH_SCOUT(null, false),
|
|
ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS(null, false),
|
|
MOVE_NO_MOVES("Attempt to move without moves left"),
|
|
MOVE_NO_ACCESS_LAND("Attempt to move a naval unit onto land"),
|
|
MOVE_NO_ACCESS_BEACHED("Attempt to move onto foreign beached ship"),
|
|
MOVE_NO_ACCESS_EMBARK("Attempt to embark onto absent or foreign carrier"),
|
|
MOVE_NO_ACCESS_FULL("Attempt to embark onto full carrier"),
|
|
MOVE_NO_ACCESS_GOODS("Attempt to trade without goods"),
|
|
MOVE_NO_ACCESS_CONTACT("Attempt to interact with natives before contact"),
|
|
MOVE_NO_ACCESS_MISSION_BAN("Attempt to use missionary at banned settlement"),
|
|
MOVE_NO_ACCESS_SETTLEMENT("Attempt to move into foreign settlement"),
|
|
MOVE_NO_ACCESS_SKILL("Attempt to learn skill with incapable unit"),
|
|
MOVE_NO_ACCESS_TRADE("Attempt to trade without authority"),
|
|
MOVE_NO_ACCESS_WAR("Attempt to trade while at war"),
|
|
MOVE_NO_ACCESS_WATER("Attempt to move into a settlement by water"),
|
|
MOVE_NO_ATTACK_CIVILIAN("Attempt to attack with civilian unit"),
|
|
MOVE_NO_ATTACK_MARINE("Attempt to attack from on board ship"),
|
|
MOVE_NO_EUROPE("Attempt to move to Europe by incapable unit"),
|
|
MOVE_NO_REPAIR("Attempt to move a unit that is under repair"),
|
|
MOVE_NO_TILE("Attempt to move when not on a tile"),
|
|
MOVE_ILLEGAL("Unspecified illegal move");
|
|
|
|
/**
|
|
* The reason why this move type is illegal.
|
|
*/
|
|
private final String reason;
|
|
|
|
/**
|
|
* Does this move type imply progress towards a destination.
|
|
*/
|
|
private final boolean progress;
|
|
|
|
MoveType(String reason) {
|
|
this.reason = reason;
|
|
this.progress = false;
|
|
}
|
|
|
|
MoveType(String reason, boolean progress) {
|
|
this.reason = reason;
|
|
this.progress = progress;
|
|
}
|
|
|
|
public boolean isLegal() {
|
|
return this.reason == null;
|
|
}
|
|
|
|
public String whyIllegal() {
|
|
return (reason == null) ? "(none)" : reason;
|
|
}
|
|
|
|
public boolean isProgress() {
|
|
return progress;
|
|
}
|
|
|
|
public boolean isAttack() {
|
|
return this == ATTACK_UNIT || this == ATTACK_SETTLEMENT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the cost of moving this {@code Unit} onto the given
|
|
* {@code Tile}. A call to {@link #getMoveType(Tile)} will return
|
|
* {@code MOVE_NO_MOVES}, if returns a move cost
|
|
* larger than the {@link #getMovesLeft moves left}.
|
|
*
|
|
* @param target The {@code Tile} this {@code Unit} will move
|
|
* onto.
|
|
* @return The cost of moving this unit onto the given {@code Tile}.
|
|
*/
|
|
public int getMoveCost(Tile target) {
|
|
return getMoveCost(getTile(), target, getMovesLeft());
|
|
}
|
|
|
|
/**
|
|
* Gets the cost of moving this {@code Unit} from the given
|
|
* {@code Tile} onto the given {@code Tile}. A call to
|
|
* {@link #getMoveType(Tile, Tile, int)} will return
|
|
* {@code MOVE_NO_MOVES}, if {@link #getMoveCost} returns a move cost
|
|
* larger than the {@link #getMovesLeft} moves left.
|
|
*
|
|
* @param from The {@code Tile} this {@code Unit} will
|
|
* move from.
|
|
* @param target The {@code Tile} this {@code Unit} will
|
|
* move onto.
|
|
* @param ml The amount of moves this Unit has left.
|
|
* @return The cost of moving this unit onto the given {@code Tile}.
|
|
*/
|
|
public int getMoveCost(Tile from, Tile target, int ml) {
|
|
// Remember to also change map.findPath(...) if you change anything
|
|
// here.
|
|
|
|
int cost = target.getType().getBasicMoveCost();
|
|
if (target.isLand() && !isNaval()) {
|
|
TileItemContainer container = target.getTileItemContainer();
|
|
if (container != null) {
|
|
cost = container.getMoveCost(from, target, cost);
|
|
}
|
|
}
|
|
|
|
if (isBeached(from)) {
|
|
// Ship on land due to it was in a colony which was abandoned
|
|
cost = ml;
|
|
} else if (cost > ml) {
|
|
// Using +2 in order to make 1/3 and 2/3 move count as
|
|
// 3/3, only when getMovesLeft > 0
|
|
if ((ml + 2 >= getInitialMovesLeft() || cost <= ml + 2
|
|
|| target.hasSettlement()) && ml != 0) {
|
|
cost = ml;
|
|
}
|
|
}
|
|
return cost;
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move made in a specified direction.
|
|
*
|
|
* @param direction The {@code Direction} of the move.
|
|
* @return The move type.
|
|
*/
|
|
public MoveType getMoveType(Direction direction) {
|
|
Tile target;
|
|
return (!hasTile())
|
|
? MoveType.MOVE_NO_TILE
|
|
: ((target = getTile().getNeighbourOrNull(direction)) == null)
|
|
? MoveType.MOVE_ILLEGAL
|
|
: getMoveType(target);
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving from one tile
|
|
* to another.
|
|
*
|
|
* @param target The target {@code Tile} of the move.
|
|
* @return The move type.
|
|
*/
|
|
public MoveType getMoveType(Tile target) {
|
|
return (!hasTile())
|
|
? MoveType.MOVE_NO_TILE
|
|
: getMoveType(getTile(), target, getMovesLeft());
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving from one tile
|
|
* to another.
|
|
*
|
|
* @param from The origin {@code Tile} of the move.
|
|
* @param target The target {@code Tile} of the move.
|
|
* @param ml The amount of moves this unit has left.
|
|
* @return The move type.
|
|
*/
|
|
public MoveType getMoveType(Tile from, Tile target, int ml) {
|
|
MoveType move = getSimpleMoveType(from, target);
|
|
if (move.isLegal()) {
|
|
switch (move) {
|
|
case ATTACK_UNIT: case ATTACK_SETTLEMENT:
|
|
// Needs only a single movement point, regardless of
|
|
// terrain, but suffers penalty.
|
|
if (ml <= 0) {
|
|
move = MoveType.MOVE_NO_MOVES;
|
|
}
|
|
break;
|
|
default:
|
|
if (ml <= 0
|
|
|| (from != null && getMoveCost(from, target, ml) > ml)) {
|
|
move = MoveType.MOVE_NO_MOVES;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return move;
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving from one tile
|
|
* to another, without checking if the unit has moves left or
|
|
* logging errors.
|
|
*
|
|
* @param from The origin {@code Tile} of the move.
|
|
* @param target The target {@code Tile} of the move.
|
|
* @return The move type, which will be one of the extended illegal move
|
|
* types on failure.
|
|
*/
|
|
public MoveType getSimpleMoveType(Tile from, Tile target) {
|
|
return (isNaval()) ? getNavalMoveType(from, target)
|
|
: getLandMoveType(from, target);
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving from one tile
|
|
* to another, without checking if the unit has moves left or
|
|
* logging errors.
|
|
*
|
|
* @param target The target {@code Tile} of the move.
|
|
* @return The move type, which will be one of the extended illegal move
|
|
* types on failure.
|
|
*/
|
|
public MoveType getSimpleMoveType(Tile target) {
|
|
return (!hasTile()) ? MoveType.MOVE_NO_TILE
|
|
: getSimpleMoveType(getTile(), target);
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move made in a specified direction,
|
|
* without checking if the unit has moves left or logging errors.
|
|
*
|
|
* @param direction The direction of the move.
|
|
* @return The move type.
|
|
*/
|
|
public MoveType getSimpleMoveType(Direction direction) {
|
|
Tile target;
|
|
return (!hasTile())
|
|
? MoveType.MOVE_NO_TILE
|
|
: ((target = getTile().getNeighbourOrNull(direction)) == null)
|
|
? MoveType.MOVE_ILLEGAL
|
|
: getSimpleMoveType(getTile(), target);
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving a naval unit
|
|
* from one tile to another.
|
|
*
|
|
* @param from The origin {@code Tile} of the move.
|
|
* @param target The target {@code Tile} of the move.
|
|
* @return The move type.
|
|
*/
|
|
private MoveType getNavalMoveType(@SuppressWarnings("unused") Tile from,
|
|
Tile target) {
|
|
if (target == null) {
|
|
return (getOwner().canMoveToEurope()) ? MoveType.MOVE_HIGH_SEAS
|
|
: MoveType.MOVE_NO_EUROPE;
|
|
} else if (isDamagedAndUnderForcedRepair()) {
|
|
return MoveType.MOVE_NO_REPAIR;
|
|
}
|
|
|
|
if (target.isLand()) {
|
|
Settlement settlement = target.getSettlement();
|
|
if (settlement == null) {
|
|
return MoveType.MOVE_NO_ACCESS_LAND;
|
|
} else if (settlement.getOwner() == getOwner()) {
|
|
return MoveType.MOVE;
|
|
} else if (isTradingUnit()) {
|
|
return getTradeMoveType(settlement);
|
|
} else {
|
|
return MoveType.MOVE_NO_ACCESS_SETTLEMENT;
|
|
}
|
|
} else { // target at sea
|
|
Unit defender = target.getFirstUnit();
|
|
if (defender != null && !getOwner().owns(defender)) {
|
|
return (isOffensiveUnit())
|
|
? MoveType.ATTACK_UNIT
|
|
: MoveType.MOVE_NO_ATTACK_CIVILIAN;
|
|
} else {
|
|
return (target.isDirectlyHighSeasConnected())
|
|
? MoveType.MOVE_HIGH_SEAS
|
|
: MoveType.MOVE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the type of a move that is made when moving a land unit to
|
|
* from one tile to another.
|
|
*
|
|
* @param from The origin {@code Tile} of the move.
|
|
* @param target The target {@code Tile} of the move.
|
|
* @return The move type.
|
|
*/
|
|
private MoveType getLandMoveType(Tile from, Tile target) {
|
|
if (target == null) return MoveType.MOVE_ILLEGAL;
|
|
|
|
Player owner = getOwner();
|
|
Unit defender = target.getFirstUnit();
|
|
|
|
if (target.isLand()) {
|
|
Settlement settlement = target.getSettlement();
|
|
if (settlement == null) {
|
|
if (defender != null && owner != defender.getOwner()) {
|
|
if (defender.isNaval()) {
|
|
return MoveType.ATTACK_UNIT;
|
|
} else if (!isOffensiveUnit()) {
|
|
return MoveType.MOVE_NO_ATTACK_CIVILIAN;
|
|
} else {
|
|
return (allowMoveFrom(from))
|
|
? MoveType.ATTACK_UNIT
|
|
: MoveType.MOVE_NO_ATTACK_MARINE;
|
|
}
|
|
} else if (target.hasLostCityRumour() && owner.isEuropean()) {
|
|
// Natives do not explore rumours
|
|
return MoveType.EXPLORE_LOST_CITY_RUMOUR;
|
|
} else {
|
|
return MoveType.MOVE;
|
|
}
|
|
} else if (owner == settlement.getOwner()) {
|
|
return MoveType.MOVE;
|
|
} else if (isTradingUnit()) {
|
|
return getTradeMoveType(settlement);
|
|
} else if (isColonist()) {
|
|
if (settlement instanceof Colony
|
|
&& hasAbility(Ability.NEGOTIATE)) {
|
|
return (allowMoveFrom(from))
|
|
? MoveType.ENTER_FOREIGN_COLONY_WITH_SCOUT
|
|
: MoveType.MOVE_NO_ACCESS_WATER;
|
|
} else if (settlement instanceof IndianSettlement
|
|
&& hasAbility(Ability.SPEAK_WITH_CHIEF)) {
|
|
return (allowMoveFrom(from))
|
|
? MoveType.ENTER_INDIAN_SETTLEMENT_WITH_SCOUT
|
|
: MoveType.MOVE_NO_ACCESS_WATER;
|
|
} else if (isOffensiveUnit()) {
|
|
return (allowMoveFrom(from))
|
|
? MoveType.ATTACK_SETTLEMENT
|
|
: MoveType.MOVE_NO_ATTACK_MARINE;
|
|
} else if (hasAbility(Ability.ESTABLISH_MISSION)) {
|
|
return getMissionaryMoveType(from, settlement);
|
|
} else {
|
|
return getLearnMoveType(from, settlement);
|
|
}
|
|
} else if (isOffensiveUnit()) {
|
|
return (allowMoveFrom(from))
|
|
? MoveType.ATTACK_SETTLEMENT
|
|
: MoveType.MOVE_NO_ATTACK_MARINE;
|
|
} else {
|
|
return MoveType.MOVE_NO_ACCESS_SETTLEMENT;
|
|
}
|
|
} else { // moving to sea, check for embarkation
|
|
return (defender == null || !getOwner().owns(defender))
|
|
? MoveType.MOVE_NO_ACCESS_EMBARK
|
|
: (any(target.getUnits(), u -> u.canAdd(this)))
|
|
? MoveType.EMBARK
|
|
: MoveType.MOVE_NO_ACCESS_FULL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the {@code MoveType} when moving a trading unit to a
|
|
* settlement.
|
|
*
|
|
* @param settlement The {@code Settlement} to move to.
|
|
* @return The appropriate {@code MoveType}.
|
|
*/
|
|
private MoveType getTradeMoveType(Settlement settlement) {
|
|
final Player owner = this.getOwner();
|
|
if (settlement instanceof Colony) {
|
|
return (owner.atWarWith(settlement.getOwner()))
|
|
? MoveType.MOVE_NO_ACCESS_WAR
|
|
: (!hasAbility(Ability.TRADE_WITH_FOREIGN_COLONIES))
|
|
? MoveType.MOVE_NO_ACCESS_TRADE
|
|
: MoveType.ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS;
|
|
} else if (settlement instanceof IndianSettlement) {
|
|
// Require contact, and settlement-level contact for naval units,
|
|
// but not block for war as bringing gifts is allowed
|
|
return (!allowContact(settlement)
|
|
|| (this.isNaval()
|
|
&& ((IndianSettlement)settlement).getContactLevel(owner)
|
|
== IndianSettlement.ContactLevel.UNCONTACTED))
|
|
? MoveType.MOVE_NO_ACCESS_CONTACT
|
|
// Allow trade if cargo present or empty-traders-option
|
|
: (this.hasGoodsCargo() || getSpecification()
|
|
.getBoolean(GameOptions.EMPTY_TRADERS))
|
|
? MoveType.ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS
|
|
: MoveType.MOVE_NO_ACCESS_GOODS;
|
|
} else {
|
|
return MoveType.MOVE_ILLEGAL; // should not happen
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the {@code MoveType} when moving a colonist to a settlement.
|
|
*
|
|
* @param from The {@code Tile} to move from.
|
|
* @param settlement The {@code Settlement} to move to.
|
|
* @return The appropriate {@code MoveType}.
|
|
*/
|
|
private MoveType getLearnMoveType(Tile from, Settlement settlement) {
|
|
if (settlement instanceof Colony) {
|
|
return MoveType.MOVE_NO_ACCESS_SETTLEMENT;
|
|
} else if (settlement instanceof IndianSettlement) {
|
|
return (!allowContact(settlement))
|
|
? MoveType.MOVE_NO_ACCESS_CONTACT
|
|
: (!allowMoveFrom(from))
|
|
? MoveType.MOVE_NO_ACCESS_WATER
|
|
: (getUnitChange(UnitChangeType.NATIVES) == null)
|
|
? MoveType.MOVE_NO_ACCESS_SKILL
|
|
: MoveType.ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST;
|
|
} else {
|
|
return MoveType.MOVE_ILLEGAL; // should not happen
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the {@code MoveType} when moving a missionary to a settlement.
|
|
*
|
|
* @param from The {@code Tile} to move from.
|
|
* @param settlement The {@code Settlement} to move to.
|
|
* @return The appropriate {@code MoveType}.
|
|
*/
|
|
private MoveType getMissionaryMoveType(Tile from, Settlement settlement) {
|
|
if (settlement instanceof Colony) {
|
|
return MoveType.MOVE_NO_ACCESS_SETTLEMENT;
|
|
} else if (settlement instanceof IndianSettlement) {
|
|
return (!allowContact(settlement))
|
|
? MoveType.MOVE_NO_ACCESS_CONTACT
|
|
: (!allowMoveFrom(from))
|
|
? MoveType.MOVE_NO_ACCESS_WATER
|
|
: (settlement.getOwner().missionsBanned(getOwner()))
|
|
? MoveType.MOVE_NO_ACCESS_MISSION_BAN
|
|
: MoveType.ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY;
|
|
} else {
|
|
return MoveType.MOVE_ILLEGAL; // should not happen
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is this unit allowed to move from a source tile?
|
|
* Implements the restrictions on moving from water.
|
|
*
|
|
* @param from The {@code Tile} to consider.
|
|
* @return True if the move is allowed.
|
|
*/
|
|
private boolean allowMoveFrom(Tile from) {
|
|
return from.isLand()
|
|
|| (!getOwner().isREF()
|
|
&& getSpecification().getBoolean(GameOptions.AMPHIBIOUS_MOVES));
|
|
}
|
|
|
|
/**
|
|
* Is this unit allowed to contact a settlement?
|
|
*
|
|
* @param settlement The {@code Settlement} to consider.
|
|
* @return True if the contact is allowed.
|
|
*/
|
|
private boolean allowContact(Settlement settlement) {
|
|
return getOwner().hasContacted(settlement.getOwner());
|
|
}
|
|
|
|
/**
|
|
* Does a basic check whether a unit can ever expect to move to a tile.
|
|
*
|
|
* @param tile The code {@code Tile} to check.
|
|
* @return True if some sort of legal move to the tile exists, including
|
|
* special cases where there is an interaction but the unit does not
|
|
* actually move, such as trade.
|
|
*/
|
|
public boolean isTileAccessible(Tile tile) {
|
|
return (isNaval())
|
|
? (!tile.isLand()
|
|
|| (tile.hasSettlement()
|
|
&& getOwner().owns(tile.getSettlement())))
|
|
: tile.isLand();
|
|
}
|
|
|
|
/**
|
|
* Gets the amount of moves this unit has at the beginning of each turn.
|
|
*
|
|
* @return The amount of moves this unit has at the beginning of
|
|
* each turn.
|
|
*/
|
|
@Override
|
|
public int getInitialMovesLeft() {
|
|
Turn turn = getGame().getTurn();
|
|
return (int)apply(this.type.getMovement(), turn, Modifier.MOVEMENT_BONUS, this.type);
|
|
}
|
|
|
|
/**
|
|
* Make a label showing the unit moves left.
|
|
*
|
|
* @return A movement label.
|
|
*/
|
|
public String getMovesAsString() {
|
|
StringBuilder sb = new StringBuilder(16);
|
|
int quotient = getMovesLeft() / 3;
|
|
int remainder = getMovesLeft() % 3;
|
|
if (quotient > 0 || remainder == 0) sb.append(quotient);
|
|
if (remainder > 0) {
|
|
sb.append('(').append(remainder).append("/3) ");
|
|
}
|
|
sb.append('/').append(getInitialMovesLeft() / 3);
|
|
return sb.toString();
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns this unit will need to sail to/from Europe.
|
|
*
|
|
* @return The number of turns to sail to/from Europe.
|
|
*/
|
|
public int getSailTurns() {
|
|
float base = getSpecification().getInteger(GameOptions.TURNS_TO_SAIL);
|
|
return (int)getOwner().apply(base, getGame().getTurn(),
|
|
Modifier.SAIL_HIGH_SEAS, this.type);
|
|
}
|
|
|
|
/**
|
|
* Checks if this {@code Unit} can be moved to the high seas
|
|
* from its current location.
|
|
*
|
|
* @return True if this unit can move immediately to the high seas.
|
|
*/
|
|
public boolean canMoveToHighSeas() {
|
|
final Predicate<Tile> highSeasMovePred = t ->
|
|
t.isDirectlyHighSeasConnected() // Quick filter before full check
|
|
&& getMoveType(t) == MoveType.MOVE_HIGH_SEAS;
|
|
return (isAtSea()) ? true
|
|
: (isInEurope()) ? getType().canMoveToHighSeas()
|
|
: (hasTile()) ? (getType().canMoveToHighSeas()
|
|
&& getOwner().canMoveToEurope()
|
|
&& (getTile().isDirectlyHighSeasConnected()
|
|
|| any(getTile().getSurroundingTiles(1, 1),
|
|
highSeasMovePred)))
|
|
: false;
|
|
}
|
|
|
|
/**
|
|
* Check if this unit can build a colony. Does not consider whether
|
|
* the tile where the unit is located is suitable,
|
|
* @see Player#canClaimToFoundSettlement(Tile)
|
|
*
|
|
* @return {@code true} if this unit can build a colony.
|
|
*/
|
|
public boolean canBuildColony() {
|
|
final Specification spec = getSpecification();
|
|
return hasTile() && this.type.canBuildColony() && getMovesLeft() > 0
|
|
&& (!getOwner().isRebel()
|
|
|| spec.getBoolean(GameOptions.FOUND_COLONY_DURING_REBELLION));
|
|
}
|
|
|
|
/**
|
|
* Is this unit at a specified location?
|
|
*
|
|
* @param loc The {@code Location} to test.
|
|
* @return True if the locations are the same, or on the same tile.
|
|
*/
|
|
public boolean isAtLocation(Location loc) {
|
|
Location ourLoc = getLocation(),
|
|
otherLoc = (loc instanceof Unit) ? ((Unit)loc).getLocation() : loc;
|
|
if (ourLoc instanceof Unit) ourLoc = ((Unit)ourLoc).getLocation();
|
|
return Map.isSameLocation(ourLoc, otherLoc);
|
|
}
|
|
|
|
/**
|
|
* Gets the best (closest) entry location for this unit to reach a
|
|
* given tile.
|
|
*
|
|
* @param tile The target {@code Tile}.
|
|
* @return The best entry location tile to arrive on the map at, or null
|
|
* if none found.
|
|
*/
|
|
public Tile getBestEntryTile(Tile tile) {
|
|
return getGame().getMap().getBestEntryTile(this, tile, null, null);
|
|
}
|
|
|
|
/**
|
|
* Resolves a destination for a unit on the high seas.
|
|
* That is, the location where the unit will appear when it leaves
|
|
* the high seas, which will either be Europe or a tile.
|
|
*
|
|
* @return The location the unit should appear next after leaving
|
|
* the high seas.
|
|
*/
|
|
public Location resolveDestination() {
|
|
if (!isAtSea()) throw new RuntimeException("Not at sea: " + this);
|
|
Tile ret = null;
|
|
// Is there a destination, either explicit or by trade route?
|
|
TradeRouteStop stop = getStop();
|
|
Location dst = (TradeRoute.isStopValid(this, stop))
|
|
? stop.getLocation()
|
|
: getDestination();
|
|
// If the destination is Europe, we are done
|
|
if (dst instanceof Europe) return dst;
|
|
// If there is a destination with an associated tile, get a
|
|
// nearby best entry tile. Otherwise use the fallback entry
|
|
// location for the owner player.
|
|
ret = (dst != null && dst.getTile() != null)
|
|
? getBestEntryTile(dst.getTile())
|
|
: getFullEntryLocation();
|
|
// Apparently this can still be null!?! At least log such cases
|
|
if (ret == null) {
|
|
logger.warning("resolveDestination(" + dst
|
|
+ ") is null for: " + this);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Set movesLeft to 0 if has some spent moves and it's in a colony
|
|
*
|
|
* @see #add(Locatable)
|
|
* @see #remove(Locatable)
|
|
*/
|
|
private void spendAllMoves() {
|
|
if (getColony() != null && getMovesLeft() < getInitialMovesLeft()) {
|
|
setMovesLeft(0);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is this unit ready to operate a trade route?
|
|
*
|
|
* @return True if the unit is ready to trade.
|
|
*/
|
|
public boolean isReadyToTrade() {
|
|
return !isDisposed()
|
|
&& !isDamagedAndUnderForcedRepair()
|
|
&& !isAtSea()
|
|
&& !isOnCarrier()
|
|
&& !isInColony()
|
|
&& getTradeRoute() != null
|
|
&& getState() != Unit.UnitState.FORTIFYING
|
|
&& getState() != Unit.UnitState.SKIPPED
|
|
&& getMovesLeft() > 0;
|
|
}
|
|
|
|
/**
|
|
* Basic checks for whether a unit is usable ATM.
|
|
*
|
|
* @return True if the unit might be useful at present.
|
|
*/
|
|
private boolean readyAndAble() {
|
|
return !isDisposed()
|
|
&& !isDamagedAndUnderForcedRepair()
|
|
&& !isAtSea()
|
|
&& !isOnCarrier()
|
|
&& !isInColony()
|
|
&& getState() == UnitState.ACTIVE
|
|
&& getMovesLeft() > 0;
|
|
}
|
|
|
|
/**
|
|
* Is this unit a suitable `next active unit', that is, the unit
|
|
* needs to be currently movable by the player.
|
|
*
|
|
* Used as a predicate in Player.nextActiveUnitIterator.
|
|
*
|
|
* @return True if this unit could still be moved by the player.
|
|
*/
|
|
public boolean isCandidateForNextActiveUnit() {
|
|
return couldMove() && !isInEurope();
|
|
}
|
|
|
|
/**
|
|
* Checks if the unit is currently movable by the player.
|
|
*
|
|
* @return True if this unit could still be moved by the player.
|
|
*/
|
|
public boolean couldMove() {
|
|
return readyAndAble()
|
|
&& getDestination() == null
|
|
&& getTradeRoute() == null;
|
|
}
|
|
|
|
/**
|
|
* Is this unit a suitable `going-to unit', that is, the unit
|
|
* needs have a valid destination and be able to progress towards it.
|
|
*
|
|
* Used as a predicate in Player.nextGoingToUnitIterator.
|
|
*
|
|
* @return True if this unit can go to its destination.
|
|
*/
|
|
public boolean goingToDestination() {
|
|
return readyAndAble()
|
|
&& getTradeRoute() == null
|
|
&& getDestination() != null;
|
|
}
|
|
|
|
/**
|
|
* Is this unit available to move along a trade route?
|
|
*
|
|
* Used as a predicate in Player.nextTradeRouteUnitIterator.
|
|
*
|
|
* @return True if this unit can follow a trade route.
|
|
*/
|
|
public boolean followingTradeRoute() {
|
|
return readyAndAble()
|
|
&& getTradeRoute() != null;
|
|
// Trade route code might set destination
|
|
}
|
|
|
|
|
|
// Map support routines
|
|
|
|
/**
|
|
* Gets a suitable location to start path searches for a unit.
|
|
*
|
|
* Must handle all the cases where the unit is off the map, and
|
|
* take account of the use of a carrier.
|
|
*
|
|
* @return A suitable starting location, or null if none found.
|
|
*/
|
|
public Location getPathStartLocation() {
|
|
final Unit carrier = getCarrier();
|
|
Location ret = getTile();
|
|
if (isOnCarrier()) {
|
|
if (ret != null) {
|
|
; // OK
|
|
} else if (carrier.getDestination() == null) {
|
|
ret = null;
|
|
} else if (carrier.getDestination() instanceof Map) {
|
|
ret = carrier.getFullEntryLocation();
|
|
} else if (carrier.getDestination() instanceof Settlement) {
|
|
ret = carrier.getDestination();
|
|
} else { // destination must be Europe
|
|
ret = null;
|
|
}
|
|
} else if (isNaval()) {
|
|
if (ret != null) {
|
|
; // OK
|
|
} else if (getDestination() == null
|
|
|| getDestination() instanceof Map) {
|
|
ret = getFullEntryLocation();
|
|
} else if (getDestination() instanceof Settlement) {
|
|
ret = getDestination();
|
|
} else {
|
|
ret = getFullEntryLocation();
|
|
}
|
|
}
|
|
if (ret != null) return ret;
|
|
|
|
// Must be a land unit not on the map. May have a carrier.
|
|
// Get our nearest settlement to Europe, fallback to any other.
|
|
final Player owner = getOwner();
|
|
Settlement sett = minimize(owner.getSettlements(),
|
|
settlementStartComparator);
|
|
if (sett == null) sett = first(owner.getSettlements());
|
|
if (sett != null) return sett;
|
|
|
|
// Owner has no settlements. If it is the REF, start from a
|
|
// rebel colony. Prefer the closest port.
|
|
if (owner.isREF()) {
|
|
return minimize(flatten(owner.getRebels(), Player::getSettlements),
|
|
settlementStartComparator);
|
|
}
|
|
|
|
// Desperately find the nearest land to the entry location.
|
|
Location loc = getFullEntryLocation();
|
|
return (loc == null || loc.getTile() == null) ? null
|
|
: find(loc.getTile().getSurroundingTiles(1, INFINITY),
|
|
Tile::isLand);
|
|
}
|
|
|
|
/**
|
|
* Should the unit use transport to get to a specified tile?
|
|
*
|
|
* True if:
|
|
* - The location is not null
|
|
* - The unit is not naval
|
|
* - The unit is not there already
|
|
* AND
|
|
* - there is no path OR the path uses an existing carrier
|
|
*
|
|
* @param loc The {@code Location} to go to.
|
|
* @return True if the unit should use transport.
|
|
*/
|
|
public boolean shouldTakeTransportTo(Location loc) {
|
|
PathNode path;
|
|
return loc != null
|
|
&& !isNaval()
|
|
&& !isAtLocation(loc)
|
|
&& ((path = this.findPath(getLocation(), loc,
|
|
getCarrier())) == null
|
|
|| path.usesCarrier());
|
|
}
|
|
|
|
/**
|
|
* Gets the trivial path for this unit. That is, the path to the
|
|
* nearest available safe settlement.
|
|
*
|
|
* @return A path to the trivial target, or null if none found.
|
|
*/
|
|
public PathNode getTrivialPath() {
|
|
if (isDisposed() || getLocation() == null) return null;
|
|
if (!isNaval()) return findOurNearestSettlement();
|
|
PathNode path = findOurNearestPort();
|
|
if (path == null) {
|
|
// This is unusual, but can happen when a ship is up a
|
|
// river and foreign ship creates a blockage downstream.
|
|
// If so, the rational thing to do is to go to a tile
|
|
// where other units can pass and which has the best
|
|
// connectivity to the high seas.
|
|
Tile tile = getTile();
|
|
if (tile != null && tile.isOnRiver()
|
|
&& tile.isHighSeasConnected()) {
|
|
path = search(getLocation(),
|
|
GoalDeciders.getCornerGoalDecider(),
|
|
CostDeciders.avoidSettlementsAndBlockingUnits(),
|
|
INFINITY, null);
|
|
if (path == null && tile.isRiverCorner()) {
|
|
// Return trivial path if already present.
|
|
return new PathNode(tile, 0, 0, false, null, null);
|
|
}
|
|
}
|
|
}
|
|
return path;
|
|
}
|
|
|
|
/**
|
|
* Get a comparator to rank locations by proximity to a start location,
|
|
* using this unit+optional carrier and cost decider.
|
|
*
|
|
* @param start The starting {@code Location}.
|
|
* @param carrier An optional carrier {@code Unit}.
|
|
* @param costDecider An option {@code CostDecider}.
|
|
* @return A suitable {@code Comparator}.
|
|
*/
|
|
private Comparator<Tile> getPathComparator(final Location start,
|
|
final Unit carrier,
|
|
final CostDecider costDecider) {
|
|
return cachingIntComparator((Tile t) -> {
|
|
PathNode p = this.findPath(start, t, carrier,
|
|
costDecider, null);
|
|
return (p == null) ? INFINITY : p.getTotalTurns();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Finds the fastest path from the current location to the
|
|
* specified one. No carrier is provided, and the default cost
|
|
* decider for this unit is used.
|
|
*
|
|
* @param end The {@code Location} in which the path ends.
|
|
* @return A {@code PathNode} from the current location to the
|
|
* end location, or null if none found.
|
|
*/
|
|
public PathNode findPath(Location end) {
|
|
return this.findPath(getLocation(), end);
|
|
}
|
|
|
|
/**
|
|
* Finds the fastest path from a given location to a specified
|
|
* one. No carrier is provided, and the default cost decider for
|
|
* this unit is used.
|
|
*
|
|
* @param start The {@code Location} at which the path starts.
|
|
* @param end The {@code Location} in which the path ends.
|
|
* @return A {@code PathNode} from the current location to the
|
|
* end location, or null if none found.
|
|
*/
|
|
public PathNode findPath(Location start, Location end) {
|
|
return this.findPath(start, end, null);
|
|
}
|
|
|
|
/**
|
|
* Finds the fastest path from a given location to a specified
|
|
* one, with an optional carrier. The default cost decider for
|
|
* the relevant unit is used.
|
|
*
|
|
* @param start The {@code Location} at which the path starts.
|
|
* @param end The {@code Location} in which the path ends.
|
|
* @param carrier An optional carrier {@code Unit} to use.
|
|
* @return A {@code PathNode} from the current location to the
|
|
* end location, or null if none found.
|
|
*/
|
|
public PathNode findPath(Location start, Location end, Unit carrier) {
|
|
return this.findPath(start, end, carrier, null, null);
|
|
}
|
|
|
|
/**
|
|
* Finds a quickest path between specified locations, optionally
|
|
* using a carrier and special purpose cost decider.
|
|
*
|
|
* @param start The {@code Location} to start at.
|
|
* @param end The {@code Location} to end at.
|
|
* @param carrier An optional carrier {@code Unit} to carry the unit.
|
|
* @param costDecider An optional {@code CostDecider} for
|
|
* determining the movement costs (uses default cost deciders
|
|
* for the unit/s if not provided).
|
|
* @param lb An optional {@code LogBuilder} to log the path to.
|
|
* @return A {@code PathNode}, or null if no path is found.
|
|
* @exception IllegalArgumentException if the destination is null,
|
|
* (FIXME) this is a temporary debugging measure.
|
|
*/
|
|
public PathNode findPath(Location start, Location end, Unit carrier,
|
|
CostDecider costDecider, LogBuilder lb) {
|
|
if (end == null) {
|
|
throw new IllegalArgumentException("findPath to null for " + this
|
|
+ " from " + start + " on " + carrier);
|
|
}
|
|
return getGame().getMap().findPath(this, realStart(start, carrier),
|
|
end, carrier, costDecider, lb);
|
|
}
|
|
|
|
/**
|
|
* Unified argument tests for full path searches, which then finds
|
|
* the actual starting location for the path. Deals with special
|
|
* cases like starting on a carrier and/or high seas.
|
|
*
|
|
* @param start The {@code Location} in which the path starts from.
|
|
* @param carrier An optional naval carrier {@code Unit} to use.
|
|
* @return The actual starting location.
|
|
* @throws IllegalArgumentException If there are any argument problems.
|
|
*/
|
|
private Location realStart(final Location start, final Unit carrier) {
|
|
if (carrier != null && !carrier.canCarryUnits()) {
|
|
throw new IllegalArgumentException("Non-carrier carrier: "
|
|
+ carrier);
|
|
} else if (carrier != null && !carrier.couldCarry(this)) {
|
|
throw new IllegalArgumentException("Carrier could not carry unit: "
|
|
+ carrier + "/" + this);
|
|
}
|
|
|
|
Location entry;
|
|
if (start == null) {
|
|
throw new IllegalArgumentException("Null start: " + this);
|
|
} else if (start instanceof Unit) {
|
|
Location unitLoc = ((Unit)start).getLocation();
|
|
if (unitLoc == null) {
|
|
throw new IllegalArgumentException("Null on-carrier start: "
|
|
+ this + "/" + start);
|
|
} else if (unitLoc instanceof HighSeas) {
|
|
if (carrier == null) {
|
|
throw new IllegalArgumentException("Null carrier when"
|
|
+ " starting on high seas: " + this);
|
|
} else if (carrier != start) {
|
|
throw new IllegalArgumentException("Wrong carrier when"
|
|
+ " starting on high seas: " + this
|
|
+ "/" + carrier + " != " + start);
|
|
}
|
|
entry = carrier.resolveDestination();
|
|
} else {
|
|
entry = unitLoc;
|
|
}
|
|
|
|
} else if (start instanceof HighSeas) {
|
|
if (isOnCarrier()) {
|
|
entry = getCarrier().resolveDestination();
|
|
} else if (isNaval()) {
|
|
entry = resolveDestination();
|
|
} else {
|
|
throw new IllegalArgumentException("No carrier when"
|
|
+ " starting on high seas: " + this
|
|
+ "/" + getLocation());
|
|
}
|
|
} else if (start instanceof Europe || start.getTile() != null) {
|
|
entry = start; // OK
|
|
} else {
|
|
throw new IllegalArgumentException("Invalid start: " + start);
|
|
}
|
|
// Valid result, reduce to tile if possible.
|
|
return (entry.getTile() != null) ? entry.getTile() : entry;
|
|
}
|
|
|
|
/**
|
|
* Convenience wrapper for the
|
|
* {@link net.sf.freecol.common.model.Map#search} function.
|
|
*
|
|
* @param start The {@code Location} to start the search from.
|
|
* @param gd The object responsible for determining whether a
|
|
* given {@code PathNode} is a goal or not.
|
|
* @param cd An optional {@code CostDecider}
|
|
* responsible for determining the path cost.
|
|
* @param maxTurns The maximum number of turns the given
|
|
* {@code Unit} is allowed to move. This is the
|
|
* maximum search range for a goal.
|
|
* @param carrier An optional naval carrier {@code Unit} to use.
|
|
* @return The path to a goal, or null if none can be found.
|
|
*/
|
|
public PathNode search(Location start, GoalDecider gd,
|
|
CostDecider cd, int maxTurns, Unit carrier) {
|
|
return (start == null) ? null
|
|
: getGame().getMap().search(this, realStart(start, carrier),
|
|
gd, cd, maxTurns, carrier, null);
|
|
}
|
|
|
|
/**
|
|
* Finds a quickest path to a neighbouring tile to a specified target
|
|
* tile, optionally using a carrier and special purpose cost decider.
|
|
*
|
|
* @param start The {@code Location} to start at.
|
|
* @param end The {@code Tile} to end at a neighbour of.
|
|
* @param carrier An optional carrier {@code Unit} to carry the unit.
|
|
* @param costDecider An optional {@code CostDecider} for
|
|
* determining the movement costs (uses default cost deciders
|
|
* for the unit/s if not provided).
|
|
* @return A {@code PathNode}, or null if no path is found.
|
|
*/
|
|
public PathNode findPathToNeighbour(Location start, Tile end, Unit carrier,
|
|
CostDecider costDecider) {
|
|
final Player owner = getOwner();
|
|
final Predicate<Tile> endPred = t ->
|
|
(isTileAccessible(t)
|
|
&& (t.getFirstUnit() == null || owner.owns(t.getFirstUnit())));
|
|
|
|
Tile best = minimize(end.getSurroundingTiles(1, 1), endPred,
|
|
getPathComparator(start, carrier, costDecider));
|
|
return (best == null) ? null
|
|
: this.findPath(start, best, carrier, costDecider, null);
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns required for this unit to reach a
|
|
* destination location from its current position. If the unit is
|
|
* currently on a carrier, it will be used.
|
|
*
|
|
* @param end The destination {@code Location}.
|
|
* @return The number of turns it will take to reach the destination,
|
|
* or {@code MANY_TURNS} if no path can be found.
|
|
*/
|
|
public int getTurnsToReach(Location end) {
|
|
return getTurnsToReach(getLocation(), end);
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns required for this unit to reach a
|
|
* destination location from a starting location. If the unit is
|
|
* currently on a carrier, it will be used.
|
|
*
|
|
* @param start The {@code Location} to start the search from.
|
|
* @param end The destination {@code Location}.
|
|
* @return The number of turns it will take to reach the {@code end},
|
|
* or {@code MANY_TURNS} if no path can be found.
|
|
*/
|
|
public int getTurnsToReach(Location start, Location end) {
|
|
return getTurnsToReach(start, end, getCarrier(),
|
|
CostDeciders.avoidSettlementsAndBlockingUnits());
|
|
}
|
|
|
|
/**
|
|
* Gets the number of turns required for this unit to reach a
|
|
* destination location from a starting location, using an optional
|
|
* carrier and cost decider.
|
|
*
|
|
* @param start The {@code Location} to start the search from.
|
|
* @param end The destination {@code Location}.
|
|
* @param carrier An optional carrier {@code Unit} to use.
|
|
* @param costDecider An optional {@code CostDecider} to
|
|
* score the path with.
|
|
* @return The number of turns it will take to reach the {@code end},
|
|
* or {@code MANY_TURNS} if no path can be found.
|
|
*/
|
|
public int getTurnsToReach(Location start, Location end, Unit carrier,
|
|
CostDecider costDecider) {
|
|
PathNode path = this.findPath(start, end, carrier, costDecider, null);
|
|
return (path == null) ? MANY_TURNS : path.getTotalTurns();
|
|
}
|
|
|
|
/**
|
|
* Get the colony that can be reached by this unit in the least number
|
|
* of turns.
|
|
*
|
|
* @param colonies A list of {@code Colony}s.
|
|
* @return The nearest {@code Colony}, or null if none found.
|
|
*/
|
|
public Colony getClosestColony(List<Colony> colonies) {
|
|
return getClosestColony(colonies.stream());
|
|
}
|
|
|
|
/**
|
|
* Get the colony that can be reached by this unit in the least number
|
|
* of turns.
|
|
*
|
|
* @param colonies A stream of {@code Colony}s.
|
|
* @return The nearest {@code Colony}, or null if none found.
|
|
*/
|
|
public Colony getClosestColony(Stream<Colony> colonies) {
|
|
final Comparator<Colony> comp = cachingIntComparator(col ->
|
|
(col == null) ? MANY_TURNS-1 : this.getTurnsToReach(col));
|
|
return minimize(concat(Stream.of((Colony)null), colonies), comp);
|
|
}
|
|
|
|
/**
|
|
* Find a path for this unit to the nearest settlement with the
|
|
* same owner that is reachable without a carrier.
|
|
*
|
|
* @param excludeStart If true, ignore any settlement the unit is
|
|
* currently in.
|
|
* @param range An upper bound on the number of moves.
|
|
* @param coastal If true, the settlement must have a path to Europe.
|
|
* @return The nearest matching settlement if any, otherwise null.
|
|
*/
|
|
public PathNode findOurNearestSettlement(final boolean excludeStart,
|
|
int range, final boolean coastal) {
|
|
final Player player = getOwner();
|
|
if (!player.hasSettlements() || !hasTile()) return null;
|
|
return findOurNearestSettlement(getTile(), excludeStart,
|
|
range, coastal);
|
|
}
|
|
|
|
/**
|
|
* Find a path for this unit to the nearest settlement with the
|
|
* same owner that is reachable without a carrier.
|
|
*
|
|
* @param startTile The {@code Tile} to start searching from.
|
|
* @param excludeStart If true, ignore any settlement the unit is
|
|
* currently in.
|
|
* @param range An upper bound on the number of moves.
|
|
* @param coastal If true, the settlement must have a path to Europe.
|
|
* @return The nearest matching settlement if any, otherwise null.
|
|
*/
|
|
public PathNode findOurNearestSettlement(final Tile startTile,
|
|
final boolean excludeStart,
|
|
int range, final boolean coastal) {
|
|
final Player player = getOwner();
|
|
if (startTile == null || !player.hasSettlements()) return null;
|
|
|
|
GoalDecider gd = new ClosestSettlementGoalDecider((excludeStart) ? startTile : null, coastal);
|
|
return search(startTile, gd, CostDeciders.avoidIllegal(), range, null);
|
|
}
|
|
|
|
/**
|
|
* Find a path for this unit to the nearest settlement with the
|
|
* same owner that is reachable without a carrier.
|
|
*
|
|
* @return A path to the nearest settlement if any, otherwise null.
|
|
*/
|
|
public PathNode findOurNearestSettlement() {
|
|
return findOurNearestSettlement(false, Integer.MAX_VALUE, false);
|
|
}
|
|
|
|
/**
|
|
* Find a path for this unit to the nearest settlement with the
|
|
* same owner that is reachable without a carrier and is connected to
|
|
* Europe by sea, or Europe if it is closer.
|
|
*
|
|
* @return A path to the nearest settlement if any, otherwise null
|
|
* (for now including if in Europe or at sea).
|
|
*/
|
|
public PathNode findOurNearestPort() {
|
|
PathNode ePath = null;
|
|
int eTurns = -1;
|
|
Europe europe = getOwner().getEurope();
|
|
if (getType().canMoveToHighSeas()) {
|
|
ePath = (europe == null) ? null : this.findPath(europe);
|
|
eTurns = (ePath == null) ? -1 : ePath.getTotalTurns();
|
|
}
|
|
PathNode sPath = findOurNearestSettlement(false, INFINITY, true);
|
|
int sTurns = (sPath == null) ? -1 : sPath.getTotalTurns();
|
|
return (ePath == null) ? sPath
|
|
: (sPath == null) ? ePath
|
|
: (sTurns <= eTurns) ? sPath : ePath;
|
|
}
|
|
|
|
/**
|
|
* Find a path to a settlement nearer to a destination.
|
|
*
|
|
* Used to find where to deliver goods to/from inland colonies,
|
|
* or when blocked.
|
|
*
|
|
* @param dst The destination {@code Location}.
|
|
* @return A path to the port, or null if none found.
|
|
*/
|
|
public PathNode findIntermediatePort(Location dst) {
|
|
final Settlement ignoreSrc = getSettlement();
|
|
final Settlement ignoreDst = dst.getSettlement();
|
|
final Tile srcTile = getTile();
|
|
final Tile dstTile = dst.getTile();
|
|
final int dstCont = (dstTile == null) ? -1 : dstTile.getContiguity();
|
|
final Comparator<Settlement> settlementComparator
|
|
= cachingIntComparator(s -> {
|
|
PathNode p = this.findPath(s);
|
|
return (p == null) ? INFINITY
|
|
: p.getTotalTurns() + dstTile.getDistanceTo(s.getTile());
|
|
});
|
|
|
|
PortMode type;
|
|
if (isNaval()) {
|
|
if (!srcTile.isHighSeasConnected()) {
|
|
// On a lake! FIXME: do better
|
|
type = PortMode.LAKE;
|
|
} else if (dstTile == null) {
|
|
// Carrier must be blocked from high seas
|
|
type = PortMode.NO_HIGH_SEAS;
|
|
} else if (dstTile.isHighSeasConnected()) {
|
|
// Carrier is blocked or destination is blocked.
|
|
type = (getTile().isOnRiver()) ? PortMode.NO_HIGH_SEAS
|
|
: PortMode.BLOCKED;
|
|
} else {
|
|
// Destination must be blocked
|
|
type = PortMode.BLOCKED;
|
|
}
|
|
} else {
|
|
if (dstTile == null || getTile().getContiguity() != dstCont) {
|
|
// Ocean travel will be required
|
|
// If already at port try to improve its connectivity,
|
|
// otherwise go to a port.
|
|
type = (srcTile.isHighSeasConnected()) ? PortMode.NO_HIGH_SEAS
|
|
: PortMode.BLOCKED;
|
|
} else {
|
|
// Pure land travel, just find a nearer settlement.
|
|
type = PortMode.LAND;
|
|
}
|
|
}
|
|
|
|
PathNode path = null;
|
|
Settlement sett;
|
|
switch (type) {
|
|
case LAKE:
|
|
// No progress possible.
|
|
break;
|
|
case NO_HIGH_SEAS:
|
|
// Starting on a river, probably blocked in there.
|
|
// Find the settlement that most reduces the high seas count.
|
|
path = search(getLocation(),
|
|
GoalDeciders.getReduceHighSeasCountGoalDecider(this),
|
|
null, INFINITY, null);
|
|
break;
|
|
case BLOCKED:
|
|
// Ocean travel required, destination blocked.
|
|
// Find the closest available connected port.
|
|
final Predicate<Settlement> portPredicate = s ->
|
|
s != ignoreSrc && s != ignoreDst;
|
|
sett = minimize(getOwner().getConnectedPortList(), portPredicate,
|
|
settlementComparator);
|
|
path = (sett == null) ? null : this.findPath(sett);
|
|
break;
|
|
case LAND:
|
|
// Land travel. Find nearby settlement with correct contiguity.
|
|
final Predicate<Settlement> contiguityPred = s ->
|
|
s != ignoreSrc && s != ignoreDst
|
|
&& s.getTile().getContiguity() == dstCont;
|
|
sett = minimize(getOwner().getSettlements(), contiguityPred,
|
|
settlementComparator);
|
|
path = (sett == null) ? null : this.findPath(sett);
|
|
break;
|
|
}
|
|
return (path != null) ? path
|
|
: findOurNearestSettlement(false, INFINITY, false);
|
|
}
|
|
|
|
/**
|
|
* Find a path for this unit to the nearest settlement with the
|
|
* same owner that is reachable without a carrier, excepting any
|
|
* on the current tile.
|
|
*
|
|
* @return The nearest settlement if any, otherwise null.
|
|
*/
|
|
public PathNode findOurNearestOtherSettlement() {
|
|
return findOurNearestSettlement(true, Integer.MAX_VALUE, false);
|
|
}
|
|
|
|
/**
|
|
* Can this unit attack a specified defender?
|
|
*
|
|
* A naval unit can never attack a land unit or settlement,
|
|
* but a land unit *can* attack a naval unit if it is beached.
|
|
* Otherwise naval units can only fight at sea, land units
|
|
* only on land.
|
|
*
|
|
* @param defender The defending {@code Unit}.
|
|
* @return True if this unit can attack.
|
|
*/
|
|
public boolean canAttack(Unit defender) {
|
|
if (defender == null || !defender.hasTile()
|
|
|| !isOffensiveUnit()) return false;
|
|
|
|
Tile tile = defender.getTile();
|
|
return (isNaval())
|
|
? !tile.hasSettlement() && defender.isNaval()
|
|
: !defender.isNaval() || defender.isBeached();
|
|
}
|
|
|
|
/**
|
|
* Searches for a unit that is a credible threatening unit to this
|
|
* unit within a range.
|
|
*
|
|
* @param range The number of turns to search for a threat in.
|
|
* @param threat The maximum tolerable probability of a potentially
|
|
* threatening unit defeating this unit in combat.
|
|
* @return A path to the threat, or null if not found.
|
|
*/
|
|
public PathNode searchForDanger(final int range, final float threat) {
|
|
final CombatModel cm = getGame().getCombatModel();
|
|
final Tile start = getTile();
|
|
final GoalDecider threatDecider = new GoalDecider() {
|
|
private PathNode found = null;
|
|
|
|
@Override
|
|
public PathNode getGoal() { return found; }
|
|
@Override
|
|
public boolean hasSubGoals() { return false; }
|
|
@Override
|
|
public boolean check(Unit unit, PathNode path) {
|
|
Tile tile = path.getTile();
|
|
if (tile == null) return false;
|
|
Unit first = tile.getFirstUnit();
|
|
if (first == null
|
|
|| !getOwner().atWarWith(first.getOwner())) {
|
|
return false;
|
|
}
|
|
final Predicate<Unit> attackerPred = u -> {
|
|
PathNode p;
|
|
return (u.canAttack(unit)
|
|
&& cm.calculateCombatOdds(u, unit).win >= threat
|
|
&& (p = u.findPath(start)) != null
|
|
&& p.getTotalTurns() < range);
|
|
};
|
|
if (any(transform(tile.getUnits(), attackerPred))) {
|
|
found = path;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
// The range to search will depend on the speed of the other
|
|
// unit. We can not know what it will be in advance, and it
|
|
// might be significantly faster than this unit. We do not
|
|
// want to just use an unbounded search range because this
|
|
// routine must be quick (especially when the supplied range
|
|
// is low). So use the heuristic of increasing the range by
|
|
// the ratio of the fastest appropriate (land/naval) unit type
|
|
// speed over the unit speed.
|
|
int reverseRange = range * (((isNaval())
|
|
? getSpecification().getFastestNavalUnitType()
|
|
: getSpecification().getFastestLandUnitType())
|
|
.getMovement()) / this.getType().getMovement();
|
|
|
|
return (start == null) ? null
|
|
: search(start, threatDecider, CostDeciders.avoidIllegal(),
|
|
reverseRange, getCarrier());
|
|
}
|
|
|
|
/**
|
|
* Checks if there is a credible threatening unit to this unit
|
|
* within a range of moves.
|
|
*
|
|
* @param range The number of turns to search for a threat within.
|
|
* @param threat The maximum tolerable probability of a potentially
|
|
* threatening unit defeating this unit in combat.
|
|
* @return True if a threat was found.
|
|
*/
|
|
public boolean isInDanger(int range, float threat) {
|
|
return searchForDanger(range, threat) != null;
|
|
}
|
|
|
|
/**
|
|
* Gets the line of sight of this {@code Unit}. That is the
|
|
* distance this {@code Unit} can sight new tiles.
|
|
*
|
|
* @return The line of sight of this {@code Unit}.
|
|
*/
|
|
public int getLineOfSight() {
|
|
final Turn turn = getGame().getTurn();
|
|
return (int)applyModifiers(this.type.getLineOfSight(), turn,
|
|
Stream.concat(this.getModifiers(Modifier.LINE_OF_SIGHT_BONUS,
|
|
this.type, turn),
|
|
((hasTile() && getTile().isExplored())
|
|
? getTile().getType().getModifiers(Modifier.LINE_OF_SIGHT_BONUS, this.type, turn)
|
|
: Stream.<Modifier>empty())));
|
|
}
|
|
|
|
/**
|
|
* Get the tiles visible to this unit.
|
|
*
|
|
* @return A set of visible {@code Tile}s.
|
|
*/
|
|
public Set<Tile> getVisibleTileSet() {
|
|
final Tile tile = getTile();
|
|
return (tile == null) ? Collections.<Tile>emptySet()
|
|
: new HashSet<Tile>(tile.getSurroundingTiles(0, getLineOfSight()));
|
|
}
|
|
|
|
|
|
// Goods handling
|
|
|
|
/**
|
|
* Get the goods carried by this unit.
|
|
*
|
|
* @param compact If true create a compact list.
|
|
* @return A list of {@code Goods}.
|
|
*/
|
|
private List<Goods> getGoodsInternal(boolean compact) {
|
|
GoodsContainer gc = getGoodsContainer();
|
|
if (gc == null) return Collections.<Goods>emptyList();
|
|
List<Goods> goods = (compact) ? gc.getCompactGoodsList()
|
|
: gc.getGoodsList();
|
|
for (Goods g : goods) g.setLocation(this);
|
|
return goods;
|
|
}
|
|
|
|
/**
|
|
* Get the goods carried by this unit.
|
|
*
|
|
* @return A list of {@code Goods}.
|
|
*/
|
|
@Override
|
|
public List<Goods> getGoodsList() {
|
|
return getGoodsInternal(false);
|
|
}
|
|
|
|
/**
|
|
* Get a compact version of the goods carried by this unit.
|
|
*
|
|
* @return A compact list of {@code Goods}.
|
|
*/
|
|
@Override
|
|
public List<Goods> getCompactGoodsList() {
|
|
return getGoodsInternal(true);
|
|
}
|
|
|
|
/**
|
|
* Can this unit carry other units?
|
|
*
|
|
* @return True if the unit can carry units.
|
|
*/
|
|
public boolean canCarryUnits() {
|
|
return hasAbility(Ability.CARRY_UNITS);
|
|
}
|
|
|
|
/**
|
|
* Could this unit carry a specified one?
|
|
* This ignores the current load.
|
|
*
|
|
* @param u The potential cargo {@code Unit}.
|
|
* @return True if this unit can carry the cargo.
|
|
*/
|
|
public boolean couldCarry(Unit u) {
|
|
return canCarryUnits()
|
|
&& getCargoCapacity() >= u.getSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* Can this unit carry goods.
|
|
*
|
|
* @return True if the unit can carry goods.
|
|
*/
|
|
public boolean canCarryGoods() {
|
|
return hasAbility(Ability.CARRY_GOODS);
|
|
}
|
|
|
|
/**
|
|
* Could this unit carry some specified goods?
|
|
* This ignores the current load.
|
|
*
|
|
* @param g The potential cargo {@code Goods}.
|
|
* @return True if this unit can carry the cargo.
|
|
*/
|
|
public boolean couldCarry(Goods g) {
|
|
return canCarryGoods()
|
|
&& getCargoCapacity() >= g.getSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* Gets the number of free cargo slots left on this unit.
|
|
*
|
|
* @return The number of free cargo slots on this unit.
|
|
*/
|
|
public int getSpaceLeft() {
|
|
return getCargoCapacity() - getCargoSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* Is there free space left on this unit?
|
|
*
|
|
* @return True if there is free space.
|
|
*/
|
|
public boolean hasSpaceLeft() {
|
|
return getSpaceLeft() > 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the total space this unit has to carry cargo.
|
|
*
|
|
* @return The total space.
|
|
*/
|
|
public int getCargoCapacity() {
|
|
return this.type.getSpace();
|
|
}
|
|
|
|
/**
|
|
* Gets the space occupied by goods in this unit. Must defend
|
|
* against goods container being null as this can be called in the
|
|
* client on foreign units, which will not have goods containers.
|
|
*
|
|
* @return The number cargo slots occupied by goods.
|
|
*/
|
|
public int getGoodsSpaceTaken() {
|
|
if (!canCarryGoods()) return 0;
|
|
GoodsContainer gc = getGoodsContainer();
|
|
return (gc == null) ? 0 : gc.getSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* Gets the space occupied by units in this unit.
|
|
*
|
|
* @return The number of cargo slots occupied by units.
|
|
*/
|
|
public int getUnitSpaceTaken() {
|
|
return (canCarryUnits()) ? sum(getUnits(), Unit::getSpaceTaken)
|
|
: 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the space occupied by cargo in this unit (both goods and units).
|
|
*
|
|
* @return The number of occupied cargo slots.
|
|
*/
|
|
public int getCargoSpaceTaken() {
|
|
return getGoodsSpaceTaken() + getUnitSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* Is this unit carrying any goods cargo?
|
|
*
|
|
* @return True if the unit is carrying any goods cargo.
|
|
*/
|
|
public boolean hasGoodsCargo() {
|
|
return getGoodsSpaceTaken() > 0;
|
|
}
|
|
|
|
/**
|
|
* Is this unit carrying any cargo (goods or unit).
|
|
*
|
|
* @return True if the unit is carrying any cargo.
|
|
*/
|
|
public boolean hasCargo() {
|
|
return getCargoSpaceTaken() > 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the extra amount of a specified type of goods that could
|
|
* be loaded onto this unit. Includes empty cargo slots and any
|
|
* spare space in a slot partially filled with the specified
|
|
* goods.
|
|
*
|
|
* @param type The {@code GoodsType} to examine.
|
|
* @return The amount of goods that could be loaded onto this unit.
|
|
*/
|
|
public int getLoadableAmount(GoodsType type) {
|
|
if (!canCarryGoods()) return 0;
|
|
int result = getSpaceLeft() * GoodsContainer.CARGO_SIZE;
|
|
int count = getGoodsCount(type) % GoodsContainer.CARGO_SIZE;
|
|
if (count != 0) result += GoodsContainer.CARGO_SIZE - count;
|
|
return result;
|
|
}
|
|
|
|
|
|
// Miscellaneous more complex functionality
|
|
|
|
/**
|
|
* Get a label for the current unit occupation.
|
|
*
|
|
* @param player The {@code Player} viewing the unit, as the
|
|
* owner has access to more information.
|
|
* @param full If true, return a more detailed result.
|
|
* @return A {@code StringTemplate} for the unit occupation.
|
|
*/
|
|
public StringTemplate getOccupationLabel(Player player, boolean full) {
|
|
final TradeRoute tradeRoute = getTradeRoute();
|
|
StringTemplate ret;
|
|
if (player != null && player.owns(this)) {
|
|
if (isDamagedAndUnderForcedRepair()) {
|
|
if (full) {
|
|
ret = StringTemplate.label(":")
|
|
.add("model.unit.occupation.underRepair")
|
|
.addName(String.valueOf(getTurnsForRepair()));
|
|
} else {
|
|
ret = StringTemplate.key("model.unit.occupation.underRepair");
|
|
}
|
|
} else if (tradeRoute != null) {
|
|
if (full) {
|
|
ret = StringTemplate.label(":")
|
|
.add("model.unit.occupation.inTradeRoute")
|
|
.addName(tradeRoute.getName());
|
|
} else {
|
|
ret = StringTemplate.key("model.unit.occupation.inTradeRoute");
|
|
}
|
|
} else if (getState() == UnitState.ACTIVE && getMovesLeft() == 0
|
|
&& !isInEurope()) {
|
|
ret = StringTemplate.key("model.unit.occupation.activeNoMovesLeft");
|
|
} else if (getState() == UnitState.IMPROVING
|
|
&& getWorkImprovement() != null) {
|
|
if (full) {
|
|
ret = StringTemplate.label(":")
|
|
.add(getWorkImprovement().getType() + ".occupationString")
|
|
.addName(String.valueOf(getWorkTurnsLeft()));
|
|
} else {
|
|
ret = StringTemplate.key(getWorkImprovement().getType() + ".occupationString");
|
|
}
|
|
} else if (getDestination() != null) {
|
|
ret = StringTemplate.key("model.unit.occupation.goingSomewhere");
|
|
} else {
|
|
ret = StringTemplate.key("model.unit." + getState().getKey());
|
|
}
|
|
} else {
|
|
if (isNaval()) {
|
|
ret = StringTemplate.name(String.valueOf(getVisibleGoodsCount()));
|
|
} else {
|
|
ret = StringTemplate.key("model.unit.occupation.activeNoMovesLeft");
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets the probability that an attack by this unit will provoke a
|
|
* native to convert.
|
|
*
|
|
* @return A probability of conversion.
|
|
*/
|
|
public float getConvertProbability() {
|
|
final Specification spec = getSpecification();
|
|
int opt = spec.getInteger(GameOptions.NATIVE_CONVERT_PROBABILITY);
|
|
return 0.01f * apply(opt, getGame().getTurn(),
|
|
Modifier.NATIVE_CONVERT_BONUS);
|
|
}
|
|
|
|
/**
|
|
* Gets the probability that an attack by this unit will provoke natives
|
|
* to burn our missions.
|
|
*
|
|
* FIXME: enhance burn probability proportionally with tension
|
|
*
|
|
* @return A probability of burning missions.
|
|
*/
|
|
public float getBurnProbability() {
|
|
final Specification spec = getSpecification();
|
|
return 0.01f * spec.getInteger(GameOptions.BURN_PROBABILITY);
|
|
}
|
|
|
|
/**
|
|
* Checks if the treasure train can be cashed in at it's current
|
|
* {@code Location}.
|
|
*
|
|
* @return {@code true} if the treasure train can be cashed in.
|
|
* @exception IllegalStateException if this unit is not a treasure train.
|
|
*/
|
|
public boolean canCashInTreasureTrain() {
|
|
return canCashInTreasureTrain(getLocation());
|
|
}
|
|
|
|
/**
|
|
* Checks if the treasure train can be cashed in at the given
|
|
* {@code Location}.
|
|
*
|
|
* @param loc The {@code Location}.
|
|
* @return {@code true} if the treasure train can be cashed in.
|
|
* @exception IllegalStateException if this unit is not a treasure train.
|
|
*/
|
|
public boolean canCashInTreasureTrain(Location loc) {
|
|
if (!canCarryTreasure()) {
|
|
throw new RuntimeException("Can't carry treasure: " + this);
|
|
}
|
|
if (loc == null) return false;
|
|
|
|
if (getOwner().getEurope() == null) {
|
|
// Any colony will do once independent, as the treasure stays
|
|
// in the New World.
|
|
return loc.getColony() != null;
|
|
}
|
|
if (loc.getColony() != null) {
|
|
// Cash in if at a colony which has connectivity to Europe
|
|
// unless the player has a suitable carrier and no free transport.
|
|
return loc.getColony().isConnectedPort()
|
|
&& (getOwner().getCarriersForUnit(this).isEmpty()
|
|
|| getTransportFee() == 0);
|
|
}
|
|
// Otherwise, cash in if in Europe.
|
|
return loc instanceof Europe
|
|
|| (loc instanceof Unit && ((Unit)loc).isInEurope());
|
|
}
|
|
|
|
/**
|
|
* Get the fee that would have to be paid to transport this
|
|
* treasure to Europe.
|
|
*
|
|
* @return The fee required for transport.
|
|
*/
|
|
public int getTransportFee() {
|
|
if (!isInEurope() && getOwner().getEurope() != null) {
|
|
float fee = (getSpecification()
|
|
.getInteger(GameOptions.TREASURE_TRANSPORT_FEE)
|
|
* getTreasureAmount()) / 100.0f;
|
|
return (int)getOwner().apply(fee, getGame().getTurn(),
|
|
Modifier.TREASURE_TRANSPORT_FEE, this.type);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the skill level.
|
|
*
|
|
* @return The level of skill for this unit. A higher value
|
|
* signals a more advanced type of units.
|
|
*/
|
|
public int getSkillLevel() {
|
|
return getUnitSkillLevel(this.type);
|
|
}
|
|
|
|
/**
|
|
* Gets the skill level of the given type of {@code Unit}.
|
|
*
|
|
* @param unitType The type of {@code Unit}.
|
|
* @return The level of skill for the given unit. A higher value
|
|
* signals a more advanced type of units.
|
|
*/
|
|
public static int getUnitSkillLevel(UnitType unitType) {
|
|
return (unitType.hasSkill()) ? unitType.getSkill() : 0;
|
|
}
|
|
|
|
/**
|
|
* Gets the appropriate trade bonuses due to a missionary unit.
|
|
*
|
|
* @param sense The sense to apply the modifiers.
|
|
* @return The missionary trade bonuses.
|
|
*/
|
|
public Set<Modifier> getMissionaryTradeModifiers(boolean sense) {
|
|
final Function<Modifier, Modifier> mapper = m -> {
|
|
Modifier mod = Modifier.makeModifier(m);
|
|
if (!sense) mod.setValue(-m.getValue());
|
|
return mod;
|
|
};
|
|
return transform(getModifiers(Modifier.MISSIONARY_TRADE_BONUS),
|
|
m -> m.getValue() != 0, mapper, Collectors.toSet());
|
|
}
|
|
|
|
/**
|
|
* Adds a feature to the Unit. This method always throws an
|
|
* {@code UnsupportedOperationException}, since features can
|
|
* not be added to Units directly.
|
|
*
|
|
* @param feature The {@code Feature} to add.
|
|
*/
|
|
public void addFeature(Feature feature) {
|
|
throw new UnsupportedOperationException("Can not add Feature to Unit directly!");
|
|
}
|
|
|
|
/**
|
|
* Gets the {@code ProductionInfo} for this unit.
|
|
*
|
|
* FIXME: the input parameter is ignored! Fix?
|
|
*
|
|
* @param input A list of input {@code AbstractGoods}.
|
|
* @return The {@code ProductionInfo} for this unit.
|
|
*/
|
|
public ProductionInfo getProductionInfo(List<AbstractGoods> input) {
|
|
ProductionInfo result = new ProductionInfo();
|
|
result.setConsumption(getType().getConsumedGoods());
|
|
result.setMaximumConsumption(getType().getConsumedGoods());
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Score this unit with its suitability for pioneering.
|
|
*
|
|
* A pioneer must be a colonst. Favour:
|
|
* - existing pioneers especially if on the map
|
|
* - expert pioneer units
|
|
* - then by skill but not other experts
|
|
*
|
|
* @return A pioneering score.
|
|
*/
|
|
public int getPioneerScore() {
|
|
int ht = (hasTile()) ? 100 : 0;
|
|
return (getLocation() == null || !isColonist()) ? -1000
|
|
: (hasAbility(Ability.IMPROVE_TERRAIN)) ? 900 + ht
|
|
: (hasAbility(Ability.EXPERT_PIONEER)) ? 700
|
|
: (!hasDefaultRole()) ? 0
|
|
: (getSkillLevel() > 0) ? 0
|
|
: 200 + getSkillLevel() * 50;
|
|
}
|
|
|
|
/**
|
|
* Score this unit with its suitability for scouting.
|
|
*
|
|
* A scout must be a colonist. Favour:
|
|
* - existing scouts especially if on the map
|
|
* - expert scouts
|
|
* - lower skill level as scouting is a good career for crims and servants
|
|
* which might become seasoned scouts
|
|
*
|
|
* @return A scouting score.
|
|
*/
|
|
public int getScoutScore() {
|
|
int ht = (hasTile()) ? 100 : 0;
|
|
return (getLocation() == null || !isColonist()) ? -1000
|
|
: (hasAbility(Ability.SPEAK_WITH_CHIEF)) ? 900 + ht
|
|
: (hasAbility(Ability.EXPERT_SCOUT)) ? 700
|
|
: (!hasDefaultRole()) ? 0
|
|
: (getSkillLevel() <= 0) ? -200 * getSkillLevel()
|
|
: 0;
|
|
}
|
|
|
|
/**
|
|
* Evaluate this unit for trade purposes.
|
|
*
|
|
* @param player The {@code Player} to evaluate for.
|
|
* @return A value of this unit.
|
|
*/
|
|
public int evaluateFor(Player player) {
|
|
final Europe europe = player.getEurope();
|
|
if (europe == null) return DEFAULT_UNIT_VALUE;
|
|
int price = europe.getUnitPrice(getType());
|
|
return (price == UNDEFINED) ? DEFAULT_UNIT_VALUE : price;
|
|
}
|
|
|
|
// @compat 0.11.0
|
|
/**
|
|
* Get modifiers required for combat.
|
|
*
|
|
* This can be replaced with just getModifiers() when accepted
|
|
* specifications have all combat modifiers with correct index
|
|
* values.
|
|
*
|
|
* @param id The identifier to get combat modifiers for.
|
|
* @param fcgot An optional {@code FreeColSpecObjectType} the
|
|
* modifier applies to.
|
|
* @param turn An optional applicable {@code Turn}.
|
|
* @return The set of {@code Modifier}s found.
|
|
*/
|
|
public Set<Modifier> getCombatModifiers(String id,
|
|
FreeColSpecObjectType fcgot, Turn turn) {
|
|
final Player owner = getOwner();
|
|
final UnitType unitType = getType();
|
|
Set<Modifier> result = new HashSet<>();
|
|
|
|
// UnitType modifiers always apply
|
|
result.addAll(transform(unitType.getModifiers(id, fcgot, turn),
|
|
alwaysTrue(),
|
|
m -> m.setModifierIndex((m.getType() == Modifier.ModifierType.ADDITIVE)
|
|
? Modifier.UNIT_ADDITIVE_COMBAT_INDEX
|
|
: Modifier.UNIT_NORMAL_COMBAT_INDEX)));
|
|
|
|
// The player's modifiers may not all apply
|
|
result.addAll(transform(owner.getModifiers(id, fcgot, turn),
|
|
alwaysTrue(),
|
|
m -> m.setModifierIndex(Modifier.GENERAL_COMBAT_INDEX)));
|
|
|
|
// Role modifiers apply
|
|
result.addAll(transform(role.getModifiers(id, fcgot, turn),
|
|
alwaysTrue(),
|
|
m -> m.setModifierIndex(Modifier.ROLE_COMBAT_INDEX)));
|
|
|
|
return result;
|
|
}
|
|
// end @compat 0.11.0
|
|
|
|
/**
|
|
* Is this unit a person that is making a given goods type, but not
|
|
* an expert at it.
|
|
*
|
|
* @param work The {@code GoodsType} to check.
|
|
* @return True if this unit is a non-expert worker.
|
|
*/
|
|
private boolean nonExpertWorker(GoodsType work) {
|
|
return isPerson() && getWorkType() == work
|
|
&& getType().getExpertProduction() != work;
|
|
}
|
|
|
|
/**
|
|
* Try to swap this unit if it is an expert for another that is
|
|
* doing its job.
|
|
*
|
|
* @param others A list of other {@code Unit}s to test against.
|
|
* @return The unit that was replaced by this expert, or null if none.
|
|
*/
|
|
public Unit trySwapExpert(List<Unit> others) {
|
|
final GoodsType work = getType().getExpertProduction();
|
|
if (work == null) return null;
|
|
final Unit other = find(others, u -> u.nonExpertWorker(work));
|
|
if (other != null) swapWork(other);
|
|
return other;
|
|
}
|
|
|
|
/**
|
|
* Swap work with another unit.
|
|
*
|
|
* @param other The other {@code Unit}.
|
|
*/
|
|
public void swapWork(Unit other) {
|
|
final Colony colony = getColony();
|
|
final Role oldRole = getRole();
|
|
final int oldRoleCount = getRoleCount();
|
|
final GoodsType work = getType().getExpertProduction();
|
|
final GoodsType oldWork = getWorkType();
|
|
Location l1 = getLocation();
|
|
Location l2 = other.getLocation();
|
|
other.setLocation(colony.getTile());
|
|
setLocation(l2);
|
|
changeWorkType(work);
|
|
other.setLocation(l1);
|
|
if (oldWork != null) other.changeWorkType(oldWork);
|
|
Role tmpRole = other.getRole();
|
|
int tmpRoleCount = other.getRoleCount();
|
|
other.changeRole(oldRole, oldRoleCount);
|
|
changeRole(tmpRole, tmpRoleCount);
|
|
}
|
|
|
|
|
|
// Message unpacking support.
|
|
|
|
/**
|
|
* Gets the tile in a given direction.
|
|
*
|
|
* @param directionString The direction.
|
|
* @return The {@code Tile} in the given direction.
|
|
* @throws IllegalStateException if there is trouble.
|
|
*/
|
|
public Tile getNeighbourTile(String directionString) {
|
|
if (!hasTile()) {
|
|
throw new IllegalStateException("Unit is not on the map: "
|
|
+ getId());
|
|
}
|
|
|
|
Direction direction = Enum.valueOf(Direction.class, directionString);
|
|
Tile tile = getTile().getNeighbourOrNull(direction);
|
|
if (tile == null) {
|
|
throw new IllegalStateException("Could not find tile"
|
|
+ " in direction: " + direction + " from unit: " + getId());
|
|
}
|
|
return tile;
|
|
}
|
|
|
|
/**
|
|
* Get a settlement by identifier, validating as much as possible.
|
|
* Designed for message unpacking where the identifier should not
|
|
* be trusted.
|
|
*
|
|
* @param <T> The {@link Settlement} type.
|
|
* @param settlementId The identifier of the {@code Settlement} to find.
|
|
* @param returnClass The expected returned settlement class.
|
|
* @return The settlement corresponding to the settlementId argument.
|
|
*/
|
|
public <T extends Settlement> T getAdjacentSettlement(String settlementId,
|
|
Class<T> returnClass) {
|
|
Game game = getOwner().getGame();
|
|
|
|
T ret = game.getFreeColGameObject(settlementId, returnClass);
|
|
if (ret == null) {
|
|
throw new IllegalStateException("Not a settlement: "
|
|
+ settlementId);
|
|
} else if (ret.getTile() == null) {
|
|
throw new IllegalStateException("Settlement is not on the map: "
|
|
+ settlementId);
|
|
} else if (!getOwner().hasContacted(ret.getOwner())) {
|
|
throw new IllegalStateException("Player " + getOwner().getId()
|
|
+ " has not contacted: " + ret.getOwner().getId());
|
|
}
|
|
|
|
if (!hasTile()) {
|
|
throw new IllegalStateException("Unit is not on the map: "
|
|
+ getId());
|
|
} else if (getTile().getDistanceTo(ret.getTile()) > 1) {
|
|
throw new IllegalStateException("Unit " + getId()
|
|
+ " is not adjacent to settlement: " + settlementId);
|
|
} else if (getOwner() == ret.getOwner()) {
|
|
throw new IllegalStateException("Unit: " + getId()
|
|
+ " and settlement: " + settlementId
|
|
+ " are both owned by player: " + getOwner().getId());
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Copy the unit, reduce visibility into any carrier and reference
|
|
* to a settlement.
|
|
*
|
|
* This is used when unit information is attached to an animation.
|
|
* The normal scope rules are inadequate there as the unit *must* be
|
|
* visible, but would normally be invisible if in a settlement or
|
|
* on a carrier.
|
|
*
|
|
* @param tile The {@code Tile} the unit appears at.
|
|
* @param player The {@code Player} the copy is for.
|
|
* @return This {@code Unit} with reduced visibility.
|
|
*/
|
|
public Unit reduceVisibility(Tile tile, Player player) {
|
|
final Game game = getGame();
|
|
Unit ret = this.copy(game, player);
|
|
if (isOnCarrier()) {
|
|
Unit carrier = getCarrier().copy(game, player);
|
|
carrier.removeAll();
|
|
carrier.add(ret);
|
|
carrier.setLocationNoUpdate(tile);
|
|
ret.setLocationNoUpdate(carrier);
|
|
} else {
|
|
ret.setLocationNoUpdate(tile);
|
|
ret.setWorkType(null);
|
|
ret.setState(Unit.UnitState.ACTIVE);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
|
|
// Interface Consumer
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public List<AbstractGoods> getConsumedGoods() {
|
|
return this.type.getConsumedGoods();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getPriority() {
|
|
return this.type.getPriority();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Stream<Modifier> getConsumptionModifiers(String id) {
|
|
return getModifiers(id);
|
|
}
|
|
|
|
|
|
// Interface Ownable
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Player getOwner() {
|
|
return owner;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* -vis: This routine has visibility implications.
|
|
*/
|
|
@Override
|
|
public void setOwner(Player player) {
|
|
this.owner = player;
|
|
}
|
|
|
|
|
|
// Interface Locatable
|
|
// getTile and getSpaceTaken are shared with Location below
|
|
|
|
/**
|
|
* Gets the location of this unit.
|
|
*
|
|
* @return The location of this {@code Unit}.
|
|
*/
|
|
@Override
|
|
public Location getLocation() {
|
|
return this.location;
|
|
}
|
|
|
|
/**
|
|
* Sets the location of this unit.
|
|
*
|
|
* -vis: This routine changes player visibility.
|
|
* -til: While units do not contribute to tile appearance as such, if
|
|
* they move in/out of a colony the visible colony size changes.
|
|
*
|
|
* @param newLocation The {@code Location} where this {@code Unit}
|
|
* is to be located.
|
|
* @return True if the location change succeeds.
|
|
*/
|
|
@Override
|
|
public boolean setLocation(Location newLocation) {
|
|
// It is possible to add a unit to a non-specific location
|
|
// within a colony by specifying the colony as the new
|
|
// location. Colony.joinColony handles this special case.
|
|
if (newLocation instanceof Colony) {
|
|
return ((Colony)newLocation).joinColony(this);
|
|
}
|
|
|
|
if (newLocation == this.location) return true;
|
|
if (newLocation != null && !newLocation.canAdd(this)) {
|
|
logger.warning("Can not add " + this + " to " + newLocation);
|
|
return false;
|
|
}
|
|
|
|
// If the unit either starts or ends this move in a colony
|
|
// then teaching status can change. However, if it moves
|
|
// between locations within the same colony with the same
|
|
// teaching ability, the teaching state should *not* change.
|
|
// We have to handle this issue here in setLocation as this is
|
|
// the only place that contains information about both
|
|
// locations.
|
|
Colony oldColony = (isInColony()) ? this.location.getColony() : null;
|
|
Colony newColony = (newLocation instanceof WorkLocation)
|
|
? newLocation.getColony() : null;
|
|
boolean withinColony = newColony != null && newColony == oldColony;
|
|
boolean preserveEducation = withinColony
|
|
&& (((WorkLocation)this.location).canTeach()
|
|
== ((WorkLocation)newLocation).canTeach());
|
|
|
|
// First disable education that will fail due to the move.
|
|
if (oldColony != null && !preserveEducation) {
|
|
oldColony.updateEducation(this, false);
|
|
}
|
|
|
|
// Move out of the old location.
|
|
if (this.location == null) {
|
|
; // do nothing
|
|
} else if (!this.location.remove(this)) {//-vis
|
|
// "Should not happen" (should always be able to remove)
|
|
throw new RuntimeException("Failed to remove " + this
|
|
+ " from " + this.location.getId());
|
|
}
|
|
|
|
// Move in to the new location.
|
|
if (newLocation == null) {
|
|
setLocationNoUpdate(null);//-vis
|
|
} else if (!newLocation.add(this)) {//-vis
|
|
// "Should not happen" (canAdd was checked above)
|
|
throw new RuntimeException("Failed to add "
|
|
+ this + " to " + newLocation.getId());
|
|
}
|
|
|
|
// See if education needs to be re-enabled.
|
|
if (newColony != null && !preserveEducation) {
|
|
newColony.updateEducation(this, true);
|
|
}
|
|
|
|
// Update population of any colonies involved.
|
|
if (!withinColony) {
|
|
if (oldColony != null) oldColony.updatePopulation();
|
|
if (newColony != null) newColony.updatePopulation();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if this {@code Unit} is located in Europe. That
|
|
* is; either directly or onboard a carrier which is in Europe.
|
|
*
|
|
* @return True if in {@code Europe}.
|
|
*/
|
|
@Override
|
|
public boolean isInEurope() {
|
|
return (isOnCarrier()) ? getCarrier().isInEurope()
|
|
: getLocation() instanceof Europe;
|
|
}
|
|
|
|
|
|
// Interface Location (from GoodsLocation via UnitLocation)
|
|
// Inherits
|
|
// FreeColObject.getId
|
|
// UnitLocation.getLocationLabelFor
|
|
// UnitLocation.contains
|
|
// UnitLocation.canAdd
|
|
// UnitLocation.getUnitCount
|
|
// UnitLocation.getUnitList
|
|
// GoodsLocation.getGoodsContainer
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Tile getTile() {
|
|
return (getLocation() != null) ? getLocation().getTile() : null;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public StringTemplate getLocationLabel() {
|
|
return StringTemplate.template("model.unit.onBoard")
|
|
.addStringTemplate("%unit%", this.getLabel());
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean add(Locatable locatable) {
|
|
if (!canAdd(locatable)) {
|
|
return false;
|
|
} else if (locatable instanceof Unit) {
|
|
Unit unit = (Unit)locatable;
|
|
if (super.add(locatable)) {
|
|
// FIXME: there seems to be an inconsistency between
|
|
// units moving from an adjacent tile onto a ship and
|
|
// units boarding a ship in-colony. The former does not
|
|
// appear to come through here (which it probably should)
|
|
// as the ship's moves do not get zeroed.
|
|
spendAllMoves();
|
|
unit.setState(UnitState.SENTRY);
|
|
return true;
|
|
}
|
|
} else if (locatable instanceof Goods) {
|
|
Goods goods = (Goods)locatable;
|
|
if (super.addGoods(goods)) {
|
|
spendAllMoves();
|
|
return true;
|
|
}
|
|
} else {
|
|
throw new IllegalStateException("Can not be added to unit: "
|
|
+ locatable);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean remove(Locatable locatable) {
|
|
if (locatable == null) {
|
|
throw new RuntimeException("Locatable must not be null: " + this);
|
|
} else if (locatable instanceof Unit && canCarryUnits()) {
|
|
if (super.remove(locatable)) {
|
|
spendAllMoves();
|
|
return true;
|
|
}
|
|
} else if (locatable instanceof Goods && canCarryGoods()) {
|
|
if (super.removeGoods((Goods)locatable) != null) {
|
|
spendAllMoves();
|
|
return true;
|
|
}
|
|
} else {
|
|
logger.warning("Tried to remove from unit: "
|
|
+ locatable);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Settlement getSettlement() {
|
|
Location loc = getLocation();
|
|
return (loc != null) ? loc.getSettlement() : null;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Location up() {
|
|
return (isInEurope()) ? getLocation().up()
|
|
: (isInColony()) ? getColony()
|
|
: (hasTile()) ? getTile().up()
|
|
: this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getRank() {
|
|
return Location.rankOf(getLocation());
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public String toShortString() {
|
|
StringBuilder sb = new StringBuilder(32);
|
|
sb.append(getId()).append('-').append(getType().getSuffix());
|
|
if (!hasDefaultRole()) {
|
|
sb.append('-').append(getRoleSuffix());
|
|
int count = getRoleCount();
|
|
if (count > 1) sb.append('.').append(count);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
|
|
// UnitLocation
|
|
// Inherits
|
|
// UnitLocation.getSpaceTaken
|
|
// UnitLocation.moveToFront
|
|
// UnitLocation.clearUnitList
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getSpaceTaken() {
|
|
// We do not have to consider what this unit is carrying
|
|
// because carriers can not be put onto carriers. Yet.
|
|
return this.type.getSpaceTaken();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public NoAddReason getNoAddReason(Locatable locatable) {
|
|
if (locatable == this) {
|
|
return NoAddReason.ALREADY_PRESENT;
|
|
} else if (locatable instanceof Unit) {
|
|
return (!canCarryUnits())
|
|
? NoAddReason.WRONG_TYPE
|
|
: (locatable.getSpaceTaken() > getSpaceLeft())
|
|
? NoAddReason.CAPACITY_EXCEEDED
|
|
: super.getNoAddReason(locatable);
|
|
} else if (locatable instanceof Goods) {
|
|
Goods goods = (Goods)locatable;
|
|
return (!canCarryGoods())
|
|
? NoAddReason.WRONG_TYPE
|
|
: (goods.getAmount() > getLoadableAmount(goods.getType()))
|
|
? NoAddReason.CAPACITY_EXCEEDED
|
|
: NoAddReason.NONE;
|
|
// Do not call super.getNoAddReason for goods because
|
|
// the capacity test in GoodsLocation.getNoAddReason does not
|
|
// account for packing and is thus too conservative.
|
|
}
|
|
return super.getNoAddReason(locatable);
|
|
}
|
|
|
|
|
|
// GoodsLocation
|
|
// Inherits
|
|
// GoodsLocation.addGoods
|
|
// GoodsLocation.removeGoods
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public void invalidateCache() {}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getGoodsCapacity() {
|
|
return getCargoCapacity();
|
|
}
|
|
|
|
public boolean canAttackRanged(Tile tile) {
|
|
return getType().getAttackRange() >= getTile().getDistanceTo(tile)
|
|
&& (
|
|
tile.getSettlement() != null && tile.getSettlement().getOwner() != getOwner()
|
|
|| tile.getDefendingUnit(this) != null && tile.getDefendingUnit(this).getOwner() != getOwner()
|
|
);
|
|
}
|
|
|
|
// Override FreeColGameObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*
|
|
* -vis: This routine can change player visibility.
|
|
*/
|
|
@Override
|
|
public void disposeResources() {
|
|
Location loc = getLocation();
|
|
if (loc != null) {
|
|
loc.remove(this);
|
|
// Do not set location to null, units that are slaughtered in
|
|
// battle need to remain valid during the animation.
|
|
}
|
|
|
|
if (teacher != null) {
|
|
teacher.setStudent(null);
|
|
teacher = null;
|
|
}
|
|
|
|
if (student != null) {
|
|
student.setTeacher(null);
|
|
student = null;
|
|
}
|
|
|
|
changeHomeIndianSettlement(null);
|
|
|
|
getOwner().removeUnit(this);
|
|
|
|
super.disposeResources();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public FreeColGameObject getLinkTarget(Player player) {
|
|
return (hasTile()) ? (FreeColGameObject)getTile().up()
|
|
: player.getEurope();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) {
|
|
IntegrityType result = super.checkIntegrity(fix, lb);
|
|
if (this.role == null) {
|
|
if (fix) {
|
|
this.role = getSpecification().getDefaultRole();
|
|
lb.add("\n Missing role set to default for: ", getId());
|
|
result = result.fix();
|
|
} else {
|
|
lb.add("\n Missing role for: ", getId());
|
|
result = result.fail();
|
|
}
|
|
}
|
|
if (this.destination != null) {
|
|
if (!((FreeColGameObject)this.destination).isInitialized()) {
|
|
if (fix) {
|
|
this.destination = null;
|
|
lb.add("\n Uninitialized destination cleared for: ",
|
|
getId());
|
|
result = result.fix();
|
|
} else {
|
|
lb.add("\n Uninitialized destination for: ", getId());
|
|
result = result.fail();
|
|
}
|
|
}
|
|
}
|
|
if (this.state == UnitState.IMPROVING
|
|
&& this.workImprovement == null) {
|
|
// This can happen as a result of trying to read an invalid
|
|
// improvement.
|
|
if (fix) {
|
|
this.state = UnitState.ACTIVE;
|
|
lb.add("\n Improving unit without improvement made active: ",
|
|
getId());
|
|
result = result.fix();
|
|
} else {
|
|
lb.add("\n Improving unit without improvement: ", getId());
|
|
result = result.fail();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// Override FreeColObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Stream <Ability> getAbilities(String id, FreeColSpecObjectType fcgot,
|
|
Turn turn) {
|
|
final Player owner = getOwner();
|
|
final UnitType unitType = getType();
|
|
|
|
return concat(
|
|
// UnitType abilities always apply.
|
|
unitType.getAbilities(id),
|
|
|
|
// Roles apply with qualification.
|
|
role.getAbilities(id, fcgot, turn),
|
|
|
|
// The player's abilities require more qualification.
|
|
owner.getAbilities(id, fcgot, turn),
|
|
|
|
// Location abilities may apply.
|
|
getLocationAbilities(id, turn));
|
|
}
|
|
|
|
/**
|
|
* Get abilities specific to this location.
|
|
*
|
|
* This is here just to simplify getAbilities(). Perhaps one day
|
|
* it could be wrapped back in, but there is unresolved complexity.
|
|
*
|
|
* FIXME: extend this to all locations? May simplify code. Units
|
|
* are also Locations however, which complicates the issue as we
|
|
* do not want Units aboard other Units to share the abilities of
|
|
* the carriers.
|
|
*
|
|
* @param id The identifier to check.
|
|
* @param turn The turn that applies.
|
|
* @return A stream of {@code Ability}s found.
|
|
*/
|
|
private Stream<Ability> getLocationAbilities(String id, Turn turn) {
|
|
final UnitType unitType = getType();
|
|
final Settlement settlement = getSettlement();
|
|
if (settlement != null) {
|
|
return settlement.getAbilities(id, unitType, turn);
|
|
}
|
|
if (isInEurope()) {
|
|
Europe europe = owner.getEurope();
|
|
if (europe != null) return europe.getAbilities(id, getType(), turn);
|
|
}
|
|
return Stream.<Ability>empty();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Stream<Modifier> getModifiers(String id, FreeColSpecObjectType fcgot,
|
|
Turn turn) {
|
|
final Player owner = getOwner();
|
|
final UnitType unitType = getType();
|
|
|
|
return concat(// UnitType modifiers always apply.
|
|
unitType.getModifiers(id, fcgot, turn),
|
|
// The player's modifiers apply.
|
|
owner.getModifiers(id, fcgot, turn),
|
|
// Role modifiers apply.
|
|
role.getModifiers(id, fcgot, turn));
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getClassIndex () {
|
|
return UNIT_CLASS_INDEX;
|
|
}
|
|
|
|
|
|
// Override FreeColObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public <T extends FreeColObject> boolean copyIn(T other) {
|
|
Unit o = copyInCast(other, Unit.class);
|
|
if (o == null || !super.copyIn(o)) return false;
|
|
final Game game = getGame();
|
|
this.name = o.getName();
|
|
this.owner = game.updateRef(o.getOwner());
|
|
this.type = o.getType();
|
|
this.state = o.getState();
|
|
this.role = o.getRole();
|
|
this.roleCount = o.getRoleCount();
|
|
this.location = game.updateLocationRef(o.getLocation());
|
|
this.entryLocation = game.updateLocationRef(o.getEntryLocation());
|
|
this.movesLeft = o.getMovesLeft();
|
|
this.workType = o.getWorkType();
|
|
this.experienceType = o.getExperienceType();
|
|
this.experience = o.getExperience();
|
|
this.workLeft = o.getWorkLeft();
|
|
// Allow creation, might be first sight
|
|
this.workImprovement = game.update(o.getWorkImprovement(), true);
|
|
this.student = game.updateRef(o.getStudent());
|
|
this.teacher = game.updateRef(o.getTeacher());
|
|
this.turnsOfTraining = o.getTurnsOfTraining();
|
|
this.nationality = o.getNationality();
|
|
this.ethnicity = o.getEthnicity();
|
|
this.indianSettlement = game.updateRef(o.getIndianSettlement());
|
|
this.hitPoints = o.getHitPoints();
|
|
this.destination = game.updateLocationRef(o.getDestination());
|
|
this.tradeRoute = game.updateRef(o.getTradeRoute());
|
|
this.currentStop = o.getCurrentStop();
|
|
this.treasureAmount = o.getTreasureAmount();
|
|
this.attrition = o.getAttrition();
|
|
this.visibleGoodsCount = o.getVisibleGoodsCount();
|
|
|
|
this.owner.addUnit(this);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public FreeColObject getDisplayObject() {
|
|
return getType();
|
|
}
|
|
|
|
|
|
// Serialization
|
|
|
|
private static final String ATTRITION_TAG = "attrition";
|
|
private static final String COUNT_TAG = "count";
|
|
private static final String CURRENT_STOP_TAG = "currentStop";
|
|
private static final String DESTINATION_TAG = "destination";
|
|
private static final String ENTRY_LOCATION_TAG = "entryLocation";
|
|
private static final String ETHNICITY_TAG = "ethnicity";
|
|
private static final String EXPERIENCE_TAG = "experience";
|
|
private static final String EXPERIENCE_TYPE_TAG = "experienceType";
|
|
private static final String HIT_POINTS_TAG = "hitPoints";
|
|
private static final String INDIAN_SETTLEMENT_TAG = "indianSettlement";
|
|
private static final String LOCATION_TAG = "location";
|
|
private static final String MOVES_LEFT_TAG = "movesLeft";
|
|
private static final String NAME_TAG = "name";
|
|
private static final String NATIONALITY_TAG = "nationality";
|
|
private static final String OWNER_TAG = "owner";
|
|
private static final String ROLE_TAG = "role";
|
|
private static final String ROLE_COUNT_TAG = "roleCount";
|
|
private static final String STATE_TAG = "state";
|
|
private static final String STUDENT_TAG = "student";
|
|
private static final String TRADE_ROUTE_TAG = "tradeRoute";
|
|
private static final String TEACHER_TAG = "teacher";
|
|
private static final String TREASURE_AMOUNT_TAG = "treasureAmount";
|
|
private static final String TURNS_OF_TRAINING_TAG = "turnsOfTraining";
|
|
private static final String UNIT_TYPE_TAG = "unitType";
|
|
private static final String VISIBLE_GOODS_COUNT_TAG = "visibleGoodsCount";
|
|
private static final String WORK_LEFT_TAG = "workLeft";
|
|
private static final String WORK_TYPE_TAG = "workType";
|
|
// @compat 0.11.0
|
|
private static final String OLD_EQUIPMENT_TAG = "equipment";
|
|
// end @compat 0.11.0
|
|
// @compat 0.11.3
|
|
private static final String OLD_TILE_IMPROVEMENT_TAG = "tileimprovement";
|
|
// end @compat 0.11.3
|
|
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
|
|
super.writeAttributes(xw);
|
|
|
|
if (name != null) xw.writeAttribute(NAME_TAG, name);
|
|
|
|
xw.writeAttribute(UNIT_TYPE_TAG, this.type);
|
|
|
|
xw.writeAttribute(MOVES_LEFT_TAG, movesLeft);
|
|
|
|
xw.writeAttribute(STATE_TAG, state);
|
|
|
|
xw.writeAttribute(ROLE_TAG, role);
|
|
|
|
xw.writeAttribute(ROLE_COUNT_TAG, roleCount);
|
|
|
|
xw.writeAttribute(HIT_POINTS_TAG, hitPoints);
|
|
|
|
if (!xw.validFor(getOwner()) && isOwnerHidden()) {
|
|
// Pirates do not disclose national characteristics.
|
|
xw.writeAttribute(OWNER_TAG, getGame().getUnknownEnemy());
|
|
|
|
} else {
|
|
xw.writeAttribute(OWNER_TAG, getOwner());
|
|
|
|
if (nationality != null) {
|
|
xw.writeAttribute(NATIONALITY_TAG, nationality);
|
|
}
|
|
|
|
if (ethnicity != null) {
|
|
xw.writeAttribute(ETHNICITY_TAG, ethnicity);
|
|
}
|
|
}
|
|
|
|
if (location != null) {
|
|
xw.writeLocationAttribute(LOCATION_TAG, location);
|
|
}
|
|
|
|
xw.writeAttribute(TREASURE_AMOUNT_TAG, treasureAmount);
|
|
|
|
if (xw.validFor(getOwner())) {
|
|
if (entryLocation != null) {
|
|
xw.writeLocationAttribute(ENTRY_LOCATION_TAG, entryLocation);
|
|
}
|
|
|
|
xw.writeAttribute(TURNS_OF_TRAINING_TAG, turnsOfTraining);
|
|
|
|
if (workType != null) xw.writeAttribute(WORK_TYPE_TAG, workType);
|
|
|
|
if (experienceType != null) {
|
|
xw.writeAttribute(EXPERIENCE_TYPE_TAG, experienceType);
|
|
}
|
|
|
|
xw.writeAttribute(EXPERIENCE_TAG, experience);
|
|
|
|
xw.writeAttribute(INDIAN_SETTLEMENT_TAG, indianSettlement);
|
|
|
|
xw.writeAttribute(WORK_LEFT_TAG, workLeft);
|
|
|
|
xw.writeAttribute(ATTRITION_TAG, attrition);
|
|
|
|
if (student != null) xw.writeAttribute(STUDENT_TAG, student);
|
|
|
|
if (teacher != null) xw.writeAttribute(TEACHER_TAG, teacher);
|
|
|
|
if (destination != null) {
|
|
xw.writeLocationAttribute(DESTINATION_TAG, destination);
|
|
}
|
|
|
|
if (tradeRoute != null) {
|
|
xw.writeAttribute(TRADE_ROUTE_TAG, tradeRoute);
|
|
|
|
xw.writeAttribute(CURRENT_STOP_TAG, currentStop);
|
|
}
|
|
|
|
} else {
|
|
if (getType().canCarryGoods()) {
|
|
xw.writeAttribute(VISIBLE_GOODS_COUNT_TAG, getVisibleGoodsCount());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
|
|
if (xw.validFor(getOwner())) {
|
|
|
|
// Do not show goods or units carried by this unit.
|
|
super.writeChildren(xw);
|
|
|
|
if (workImprovement != null) workImprovement.toXML(xw);
|
|
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
|
|
super.readAttributes(xr);
|
|
|
|
final Specification spec = getSpecification();
|
|
final Game game = getGame();
|
|
final WorkLocation oldWorkLocation = getWorkLocation();
|
|
|
|
name = xr.getAttribute(NAME_TAG, (String)null);
|
|
|
|
Player oldOwner = owner;
|
|
owner = xr.findFreeColGameObject(game, OWNER_TAG,
|
|
Player.class, (Player)null, true);
|
|
|
|
this.type = xr.getType(spec, UNIT_TYPE_TAG,
|
|
UnitType.class, (UnitType)null);
|
|
|
|
state = xr.getAttribute(STATE_TAG, UnitState.class, UnitState.ACTIVE);
|
|
|
|
role = xr.getType(spec, ROLE_TAG, Role.class, spec.getDefaultRole());
|
|
|
|
roleCount = xr.getAttribute(ROLE_COUNT_TAG, role.getMaximumCount());
|
|
|
|
setLocationNoUpdate(xr.getLocationAttribute(game, LOCATION_TAG, true));
|
|
|
|
entryLocation = xr.getLocationAttribute(game, ENTRY_LOCATION_TAG,
|
|
true);
|
|
|
|
movesLeft = xr.getAttribute(MOVES_LEFT_TAG, 0);
|
|
|
|
workLeft = xr.getAttribute(WORK_LEFT_TAG, 0);
|
|
|
|
attrition = xr.getAttribute(ATTRITION_TAG, 0);
|
|
|
|
nationality = xr.getAttribute(NATIONALITY_TAG, (String)null);
|
|
|
|
ethnicity = xr.getAttribute(ETHNICITY_TAG, (String)null);
|
|
|
|
turnsOfTraining = xr.getAttribute(TURNS_OF_TRAINING_TAG, 0);
|
|
|
|
hitPoints = xr.getAttribute(HIT_POINTS_TAG, -1);
|
|
|
|
teacher = xr.makeFreeColObject(game, TEACHER_TAG, Unit.class, false);
|
|
|
|
student = xr.makeFreeColObject(game, STUDENT_TAG, Unit.class, false);
|
|
|
|
setHomeIndianSettlement(xr.makeFreeColObject(game,
|
|
INDIAN_SETTLEMENT_TAG, IndianSettlement.class, false));
|
|
|
|
treasureAmount = xr.getAttribute(TREASURE_AMOUNT_TAG, 0);
|
|
|
|
destination = xr.getLocationAttribute(game, DESTINATION_TAG, true);
|
|
|
|
tradeRoute = xr.findFreeColGameObject(game, TRADE_ROUTE_TAG,
|
|
TradeRoute.class, (TradeRoute)null, false);
|
|
|
|
currentStop = (tradeRoute == null) ? -1
|
|
: xr.getAttribute(CURRENT_STOP_TAG, 0);
|
|
|
|
experienceType = xr.getType(spec, EXPERIENCE_TYPE_TAG,
|
|
GoodsType.class, (GoodsType)null);
|
|
if (experienceType == null && workType != null) {
|
|
experienceType = workType;
|
|
}
|
|
|
|
experience = xr.getAttribute(EXPERIENCE_TAG, 0);
|
|
|
|
visibleGoodsCount = xr.getAttribute(VISIBLE_GOODS_COUNT_TAG, -1);
|
|
|
|
setWorkType(xr.getType(spec, WORK_TYPE_TAG, GoodsType.class, null));
|
|
|
|
// Fix changes to production
|
|
WorkLocation wl = getWorkLocation();
|
|
if (wl != null && wl != oldWorkLocation) wl.updateProductionType();
|
|
|
|
// Check ownership as late as possible
|
|
if (xr.shouldIntern()) game.checkOwners(this, oldOwner);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readChildren(FreeColXMLReader xr) throws XMLStreamException {
|
|
// Clear containers.
|
|
if (getGoodsContainer() != null) getGoodsContainer().removeAll();
|
|
workImprovement = null;
|
|
|
|
super.readChildren(xr);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
|
|
final Game game = getGame();
|
|
final String tag = xr.getLocalName();
|
|
|
|
// @compat 0.11.0
|
|
if (OLD_EQUIPMENT_TAG.equals(tag)) {
|
|
xr.swallowTag(OLD_EQUIPMENT_TAG);
|
|
// end @compat 0.11.0
|
|
|
|
} else if (TileImprovement.TAG.equals(tag)
|
|
// @compat 0.11.3
|
|
|| OLD_TILE_IMPROVEMENT_TAG.equals(tag)
|
|
// end @compat 0.11.3
|
|
) {
|
|
workImprovement = xr.readFreeColObject(game, TileImprovement.class);
|
|
|
|
} else {
|
|
super.readChild(xr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public String getXMLTagName() { return TAG; }
|
|
|
|
|
|
// Override Object
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
return toString("");
|
|
}
|
|
|
|
/**
|
|
* Gets a string representation of this unit.
|
|
*
|
|
* @param prefix A prefix (e.g. "AIUnit")
|
|
* @return A string representation of this {@code Unit}.
|
|
*/
|
|
public String toString(String prefix) {
|
|
StringBuilder sb = new StringBuilder(64);
|
|
sb.append('[').append(prefix).append(getId());
|
|
if (!isInitialized()) {
|
|
sb.append(" uninitialized");
|
|
} else if (isDisposed()) {
|
|
sb.append(" disposed");
|
|
} else if (owner == null) {
|
|
sb.append(" unowned");
|
|
} else if (getType() == null) {
|
|
sb.append(" untyped");
|
|
} else {
|
|
sb.append(' ').append(lastPart(owner.getNationId(), "."))
|
|
.append(' ').append(getType().getSuffix());
|
|
if (!hasDefaultRole()) {
|
|
sb.append('-').append(getRoleSuffix());
|
|
int count = getRoleCount();
|
|
if (count > 1) sb.append('.').append(count);
|
|
}
|
|
sb.append(' ').append(getMovesAsString());
|
|
}
|
|
sb.append(']');
|
|
return sb.toString();
|
|
}
|
|
}
|