/**
* Copyright (C) 2002-2012 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 java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.freecolandroid.xml.stream.XMLStreamConstants;
import org.freecolandroid.xml.stream.XMLStreamException;
import org.freecolandroid.xml.stream.XMLStreamReader;
import org.freecolandroid.xml.stream.XMLStreamWriter;
import net.sf.freecol.common.model.Map.Direction;
import net.sf.freecol.common.model.pathfinding.CostDecider;
import net.sf.freecol.common.model.pathfinding.GoalDecider;
import net.sf.freecol.common.model.TradeRoute.Stop;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.util.EmptyIterator;
import org.w3c.dom.Element;
/**
* Represents all pieces that can be moved on the map-board. This includes:
* colonists, ships, wagon trains e.t.c.
*
* <br>
* <br>
*
* Every <code>Unit</code> is owned by a {@link Player} and has a
* {@link Location}.
*/
public class Unit extends FreeColGameObject
implements Consumer, Locatable, Location, Movable, Nameable, Ownable {
private static Comparator<Unit> skillLevelComp =
new Comparator<Unit>() {
public int compare(Unit u1, Unit u2) {
return u1.getSkillLevel() - u2.getSkillLevel();
}
};
private static final Logger logger = Logger.getLogger(Unit.class.getName());
/**
* XML tag name for equipment list.
*/
private static final String EQUIPMENT_TAG = "equipment";
public static final String CARGO_CHANGE = "CARGO_CHANGE";
public static final String EQUIPMENT_CHANGE = "EQUIPMENT_CHANGE";
/**
* A state a Unit can have.
*/
public static enum UnitState {
ACTIVE,
FORTIFIED,
SENTRY,
IN_COLONY,
IMPROVING,
// @compat 0.10.0
TO_EUROPE,
TO_AMERICA,
// end compatibility code
FORTIFYING,
SKIPPED
}
/**
* The roles a Unit can have. TODO: make this configurable by
* adding roles to the specification instead of using an enum. New
* roles, such as mounted pioneers, have already been suggested.
*/
public static enum Role {
DEFAULT, PIONEER, MISSIONARY, SOLDIER, SCOUT, DRAGOON;
// Equipment types needed for certain roles.
private static final HashMap<Role, List<EquipmentType>> roleEquipment
= new HashMap<Role, List<EquipmentType>>();
/**
* Initializes roleEquipment. How about that.
*/
private void initializeRoleEquipment(Specification spec) {
if (!roleEquipment.isEmpty()) return;
UnitType defaultUnit = spec.getDefaultUnitType();
for (EquipmentType e : spec.getEquipmentTypeList()) {
Role r = e.getRole();
if (r != null) {
List<EquipmentType> eq = roleEquipment.get(r);
if (eq == null) {
eq = new ArrayList<EquipmentType>();
roleEquipment.put(r, eq);
}
eq.add(e);
}
}
// TODO: Not quite completely generic yet. There are more
// equipment types that are compatible with the dragoon role.
// The spec expresses this with <compatible-equipment> but
// it does not express that while muskets and horses are compatible
// for a soldier, they are not for a scout.
for (EquipmentType e : spec.getEquipmentTypeList()) {
if (!e.isMilitaryEquipment()) continue;
List<EquipmentType> eq = roleEquipment.get(Role.DRAGOON);
if (eq == null) {
eq = new ArrayList<EquipmentType>();
roleEquipment.put(Role.DRAGOON, eq);
}
if (!eq.contains(e)) eq.add(e);
}
// Make sure there is an empty list at least for each role.
for (Role r : Role.values()) {
List<EquipmentType> e = roleEquipment.get(r);
if (e == null) {
e = new ArrayList<EquipmentType>();
roleEquipment.put(r, e);
}
}
}
/**
* Gets the equipment required for this role.
* TODO: passing the spec in is a wart.
*
* @param spec The <code>Specification</code> to extract requirements
* from.
* @return A list of required <code>EquipmentType</code>s.
*/
public List<EquipmentType> getRoleEquipment(Specification spec) {
initializeRoleEquipment(spec);
return new ArrayList<EquipmentType>(roleEquipment.get(this));
}
public boolean isCompatibleWith(Role oldRole) {
return (this == oldRole) ||
(this == SOLDIER && oldRole == DRAGOON) ||
(this == DRAGOON && oldRole == SOLDIER);
}
public Role newRole(Role role) {
if (this == SOLDIER && role == SCOUT) {
return DRAGOON;
} else if (this == SCOUT && role == SOLDIER) {
return DRAGOON;
} else {
return role;
}
}
public String getId() {
return toString().toLowerCase(Locale.US);
}
}
/**
* A move type.
*
* @see Unit#getMoveType(Map.Direction)
*/
public static 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_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_ILLEGAL("Unspecified illegal move");
/**
* The reason why this move type is illegal.
*/
private String reason;
/**
* Does this move type imply progress towards a destination.
*/
private 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;
}
}
protected UnitType unitType;
protected int movesLeft;
protected UnitState state = UnitState.ACTIVE;
protected Role role = Role.DEFAULT;
/**
* The number of turns until the work is finished, or '-1' if a
* Unit can stay in its state forever.
*/
protected int workLeft;
protected int hitpoints; // For now; only used by ships when repairing.
protected Player owner;
protected String nationality = null;
protected String ethnicity = null;
protected List<Unit> units = Collections.emptyList();
protected GoodsContainer goodsContainer;
protected Location entryLocation;
protected Location location;
protected IndianSettlement indianSettlement = null; // only used by Brave and Convert
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;
/**
* What is being improved (to be used only for PIONEERs - where
* they are working.
*/
protected TileImprovement workImprovement;
/** What type of goods this unit produces in its occupation. */
protected GoodsType workType;
/** What type of goods this unit last earned experience producing. */
private GoodsType experienceType;
protected int experience = 0;
protected int turnsOfTraining = 0;
/**
* 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 individual name of this unit, not of the unit type. */
protected String name = null;
/**
* 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;
/** The student of this Unit, if it has one. */
protected Unit student;
/** The teacher of this Unit, if it has one. */
protected Unit teacher;
/** The equipment this Unit carries. */
protected TypeCountMap<EquipmentType> equipment
= new TypeCountMap<EquipmentType>();
/**
* Constructor for ServerUnit.
*/
protected Unit() {
// empty constructor
}
/**
* Constructor for ServerUnit.
*
* @param game The <code>Game</code> in which this unit belongs.
*/
protected Unit(Game game) {
super(game);
}
/**
* Initialize this object from an XML-representation of this object.
*
* @param game The <code>Game</code> in which this <code>Unit</code>
* belong.
* @param in The input stream containing the XML.
* @throws XMLStreamException if a problem was encountered during parsing.
*/
public Unit(Game game, XMLStreamReader in) throws XMLStreamException {
super(game, in);
readFromXML(in);
}
/**
* Initialize this object from an XML-representation of this object.
*
* @param game The <code>Game</code> in which this <code>Unit</code>
* belong.
* @param e An XML-element that will be used to initialize this object.
*/
public Unit(Game game, Element e) {
super(game, e);
readFromXMLElement(e);
}
/**
* Initiates a new <code>Unit</code> with the given ID. The object should
* later be initialized by calling either
* {@link #readFromXML(XMLStreamReader)} or
* {@link #readFromXMLElement(Element)}.
*
* @param game The <code>Game</code> in which this object belong.
* @param id The unique identifier for this object.
*/
public Unit(Game game, String id) {
super(game, id);
}
/**
* Returns <code>true</code> if the Unit can carry other Units.
*
* @return a <code>boolean</code> value
*/
public boolean canCarryUnits() {
return hasAbility(Ability.CARRY_UNITS);
}
/**
* Is this unit able to carry a specified one?
*
* @param The potential cargo <code>Unit</code>.
* @return True if this unit can carry the cargo unit.
*/
public boolean canCarryUnit(Unit u) {
return canCarryUnits() && getType().getSpace() >= u.getSpaceTaken();
}
/**
* Returns <code>true</code> if the Unit can carry Goods.
*
* @return a <code>boolean</code> value
*/
public boolean canCarryGoods() {
return hasAbility(Ability.CARRY_GOODS);
}
/**
* Returns a name for this unit, as a location.
*
* @return A name for this unit, as a location.
*/
public StringTemplate getLocationName() {
return StringTemplate.template("onBoard")
.addStringTemplate("%unit%", getLabel());
}
/**
* Returns the name for this unit, as a location, for a particular player.
*
* @param player The <code>Player</code> to prepare the name for.
* @return A name for this unit, as a location.
*/
public StringTemplate getLocationNameFor(Player player) {
return getLocationName();
}
/**
* Get the <code>UnitType</code> value.
*
* @return an <code>UnitType</code> value
*/
public final UnitType getType() {
return unitType;
}
/**
* Returns the current amount of treasure in this unit. Should be type of
* TREASURE_TRAIN.
*
* @return The amount of treasure.
*/
public int getTreasureAmount() {
if (canCarryTreasure()) {
return treasureAmount;
}
throw new IllegalStateException("Unit can't carry treasure");
}
/**
* The current amount of treasure in this unit. Should be type of
* TREASURE_TRAIN.
*
* @param amt The amount of treasure
*/
public void setTreasureAmount(int amt) {
if (canCarryTreasure()) {
this.treasureAmount = amt;
} else {
throw new IllegalStateException("Unit can't carry treasure");
}
}
/**
* Get the <code>Equipment</code> value.
*
* @return a <code>List<EquipmentType></code> value
*/
public final TypeCountMap<EquipmentType> getEquipment() {
return equipment;
}
/**
* Set the <code>Equipment</code> value.
*
* @param newEquipment The new Equipment value.
*/
public final void setEquipment(final TypeCountMap<EquipmentType> newEquipment) {
this.equipment = newEquipment;
}
/**
* Clears all <code>Equipment</code> held by this unit.
*/
public void clearEquipment() {
setEquipment(new TypeCountMap<EquipmentType>());
}
/**
* Get the <code>TradeRoute</code> value.
*
* @return a <code>TradeRoute</code> value
*/
public final TradeRoute getTradeRoute() {
return tradeRoute;
}
/**
* Set the <code>TradeRoute</code> value.
*
* @param newTradeRoute The new 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 stop.
*/
public Stop getStop() {
return (validateCurrentStop() < 0) ? null
: getTradeRoute().getStops().get(currentStop);
}
/**
* Get the current stop.
*
* @return The current stop (an index in stops).
*/
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 (an index in stops).
*/
public int validateCurrentStop() {
if (tradeRoute == null) {
currentStop = -1;
} else {
List<Stop> stops = tradeRoute.getStops();
if (stops == null || stops.size() == 0) {
currentStop = -1;
} else {
if (currentStop < 0 || currentStop >= stops.size()) {
// The current stop can become out of range if the trade
// route is modified.
currentStop = 0;
}
}
}
return currentStop;
}
/**
* Checks if the treasure train can be cashed in at it's current
* <code>Location</code>.
*
* @return <code>true</code> 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</code>.
*
* @param loc The <code>Location</code>.
* @return <code>true</code> 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 IllegalStateException("Can't carry treasure");
}
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
return loc.getColony().isConnected();
}
// Otherwise, cash in if in Europe.
return loc instanceof Europe
|| (loc instanceof Unit && ((Unit)loc).isInEurope());
}
/**
* Return the fee that would have to be paid to transport this
* treasure to Europe.
*
* @return an <code>int</code> value
*/
public int getTransportFee() {
if (!isInEurope() && getOwner().getEurope() != null) {
float fee = (getSpecification().getInteger("model.option.treasureTransportFee")
* getTreasureAmount()) / 100;
return (int) getOwner().getFeatureContainer()
.applyModifier(fee,
"model.modifier.treasureTransportFee",
unitType, getGame().getTurn());
}
return 0;
}
/**
* Checks if this is a trading <code>Unit</code>, 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</code> is a `colonist'. A unit is a
* colonist if it is European and can build a new <code>Colony</code>.
*
* @return <i>true</i> if this unit is a colonist and <i>false</i>
* otherwise.
*/
public boolean isColonist() {
return unitType.hasAbility("model.ability.foundColony")
&& owner.isEuropean();
}
/**
* Checks if this is an offensive unit. That is, one that can
* attack other units.
*
* @return <code>true</code> if this is an offensive unit.
*/
public boolean isOffensiveUnit() {
return unitType.getOffence() > UnitType.DEFAULT_OFFENCE
|| isArmed() || isMounted();
}
/**
* 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 = getNeededTurnsOfTraining(unitType, student.unitType);
if (getColony() != null) {
result -= getColony().getProductionBonus();
}
}
return result;
}
/**
* 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
*
* @param typeTeacher the unit type of the teacher
* @param typeStudent the unit type of the student
* @return an <code>int</code> value
*/
public int getNeededTurnsOfTraining(UnitType typeTeacher, UnitType typeStudent) {
UnitType teaching = getUnitTypeTeaching(typeTeacher, typeStudent);
if (teaching != null) {
return typeStudent.getEducationTurns(teaching);
} else {
throw new IllegalStateException("typeTeacher=" + typeTeacher + " typeStudent=" + typeStudent);
}
}
/**
* Gets the UnitType which a teacher is teaching to a student.
* This value is only meaningful for teachers that can be put in a
* school.
*
* @param typeTeacher the unit type of the teacher
* @param typeStudent the unit type of the student
* @return an <code>UnitType</code> value
* @see #getTurnsOfTraining
*
*/
public static UnitType getUnitTypeTeaching(UnitType typeTeacher, UnitType typeStudent) {
UnitType skillTaught = typeTeacher.getSkillTaught();
if (typeStudent.canBeUpgraded(skillTaught, ChangeType.EDUCATION)) {
return skillTaught;
} else {
return typeStudent.getEducationUnit(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 getSkillLevel(unitType);
}
/**
* Gets the skill level of the given type of <code>Unit</code>.
*
* @param unitType The type of <code>Unit</code>.
* @return The level of skill for the given unit. A higher value signals a
* more advanced type of units.
*/
public static int getSkillLevel(UnitType unitType) {
if (unitType.hasSkill()) {
return unitType.getSkill();
}
return 0;
}
/**
* Returns a Comparator that compares the skill levels of given
* units.
*
* @return skill Comparator
*/
public static Comparator<Unit> getSkillLevelComparator() {
return skillLevelComp;
}
/**
* Gets the number of turns this unit has been training.
*
* @return The number of turns of training this <code>Unit</code> 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</code> has given.
* @see #getNeededTurnsOfTraining
*/
public void setTurnsOfTraining(int turnsOfTraining) {
this.turnsOfTraining = turnsOfTraining;
}
/**
* Gets the experience of this <code>Unit</code> at its current
* experienceType.
*
* @return The experience of this <code>Unit</code> at its current
* experienceType.
* @see #modifyExperience
*/
public int getExperience() {
return experience;
}
/**
* Sets the experience of this <code>Unit</code> at its current
* experienceType.
*
* @param experience The new experience of this <code>Unit</code>
* 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</code> at its current
* experienceType.
*
* @param value The value by which to modify the experience of this
* <code>Unit</code>.
* @see #getExperience
*/
public void modifyExperience(int value) {
experience += value;
}
/**
* 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;
}
/**
* Returns true if the Unit, or its owner has the ability
* identified by <code>id</code>.
*
* @param id a <code>String</code> value
* @return a <code>boolean</code> value
*/
public boolean hasAbility(String id) {
Set<Ability> result = new HashSet<Ability>();
// UnitType abilities always apply
result.addAll(unitType.getFeatureContainer().getAbilitySet(id));
// the player's abilities may not apply
result.addAll(getOwner().getFeatureContainer()
.getAbilitySet(id, unitType, getGame().getTurn()));
// EquipmentType abilities always apply
for (EquipmentType equipmentType : equipment.keySet()) {
result.addAll(equipmentType.getFeatureContainer().getAbilitySet(id));
// player abilities may also apply to equipment (missionary)
result.addAll(getOwner().getFeatureContainer()
.getAbilitySet(id, equipmentType, getGame().getTurn()));
}
// Location abilities may apply. TODO: extend this to all
// locations? May simplify code. Units are also Locations,
// however, which complicates the issue. We do not want Units
// aboard other Units to share the abilities of the carriers.
if (getColony() != null) {
result.addAll(getColony().getFeatureContainer()
.getAbilitySet(id, unitType, getGame().getTurn()));
} else if (isInEurope()) {
// TODO: the following check should not be necessary
// Presumably, it will become redundant as soon as
// Europe abilities are in the spec
if (getOwner().getEurope() != null
&& getOwner().getEurope().getFeatureContainer() != null) {
result.addAll(getOwner().getEurope().getFeatureContainer()
.getAbilitySet(id, unitType, getGame().getTurn()));
}
}
return FeatureContainer.hasAbility(result);
}
/**
* Get a modifier that applies to this Unit.
*
* @param id a <code>String</code> value
* @return a <code>Modifier</code> value
*/
public Set<Modifier> getModifierSet(String id) {
Set<Modifier> result = new HashSet<Modifier>();
// UnitType modifiers always apply
result.addAll(unitType.getModifierSet(id));
// the player's modifiers may not apply
result.addAll(getOwner().getFeatureContainer()
.getModifierSet(id, unitType, getGame().getTurn()));
// EquipmentType modifiers always apply
for (EquipmentType equipmentType : equipment.keySet()) {
result.addAll(equipmentType.getFeatureContainer().getModifierSet(id));
// player modifiers may also apply to equipment (unused)
result.addAll(getOwner().getFeatureContainer()
.getModifierSet(id, equipmentType, getGame().getTurn()));
}
return result;
}
/**
* Get a modifier that applies to the given Ownable. This is used
* for the offenceAgainst and defenceAgainst modifiers.
*
* @param id a <code>String</code> value
* @param ownable a <code>Ownable</code> value
* @return a <code>Modifier</code> value
*/
public Set<Modifier> getModifierSet(String id, Ownable ownable) {
Set<Modifier> result = new HashSet<Modifier>();
NationType nationType = ownable.getOwner().getNationType();
Turn turn = getGame().getTurn();
result.addAll(unitType.getFeatureContainer().getModifierSet(id, nationType, turn));
result.addAll(getOwner().getFeatureContainer().getModifierSet(id, nationType, turn));
for (EquipmentType equipmentType : equipment.keySet()) {
result.addAll(equipmentType.getFeatureContainer().getModifierSet(id, nationType, turn));
}
return result;
}
/**
* Add the given Feature to the Features Map. If the Feature given
* can not be combined with a Feature with the same ID already
* present, the old Feature will be replaced.
*
* @param feature a <code>Feature</code> value
*/
public void addFeature(Feature feature) {
throw new UnsupportedOperationException("Can not add Feature to Unit directly!");
}
/**
* Returns true if this unit can be a student.
*
* @param teacher the teacher which is trying to teach it
* @return a <code>boolean</code> value
*/
public boolean canBeStudent(Unit teacher) {
return teacher != this && canBeStudent(unitType, teacher.unitType);
}
/**
* Returns true if this type of unit can be a student.
*
* @param typeStudent the unit type of the student
* @param typeTeacher the unit type of the teacher which is trying to teach it
* @return a <code>boolean</code> value
*/
public boolean canBeStudent(UnitType typeStudent, UnitType typeTeacher) {
return getUnitTypeTeaching(typeTeacher, typeStudent) != null;
}
/**
* Get the <code>Student</code> value.
*
* @return an <code>Unit</code> value
*/
public final Unit getStudent() {
return student;
}
/**
* Set the <code>Student</code> value.
*
* @param newStudent The new Student value.
*/
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 <code>Teacher</code> value.
*
* @return an <code>Unit</code> value
*/
public final Unit getTeacher() {
return teacher;
}
/**
* Set the <code>Teacher</code> value.
*
* @param newTeacher The new Teacher value.
*/
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 <code>Building</code> this unit is working in.
* TODO: migrate usage of this to getWorkBuilding(), then delete this and rename the method below
*/
public Building getWorkLocation() {
if (getLocation() instanceof Building) {
return ((Building) getLocation());
}
return null;
}
/**
* Gets the <code>Location</code> this unit is working in.
*/
public Location getWorkLocation2() {
if (getLocation() instanceof Building) {
return getLocation();
}
else if (getLocation() instanceof ColonyTile) {
return getLocation();
}
return null;
}
/**
* Gets the <code>Building</code> this unit is working in.
*/
public Building getWorkBuilding() {
if (getLocation() instanceof Building) {
return ((Building) getLocation());
}
return null;
}
/**
* Gets the <code>ColonyTile</code> this unit is working in.
*/
public ColonyTile getWorkTile() {
if (getLocation() instanceof ColonyTile) {
return ((ColonyTile) getLocation());
}
return null;
}
/**
* Gets the type of goods this unit has accrued experience producing.
*
* @return The type of goods this unit would produce.
*/
public GoodsType getExperienceType() {
return experienceType;
}
/**
* 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;
}
/**
* Sets the type of goods this unit is producing in its current occupation.
*
* @param type The type of goods to attempt to produce.
*/
public void setWorkType(GoodsType type) {
workType = type;
if (type != null) {
experienceType = type;
}
}
/**
* Gets the TileImprovement that this pioneer is contributing to.
*
* @return The <code>TileImprovement</code> 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</code> the pioneer is to
* work on.
*/
public void setWorkImprovement(TileImprovement imp) {
workImprovement = imp;
}
/**
* Returns the destination of this unit.
*
* @return The destination of this unit.
*/
public Location getDestination() {
return destination;
}
/**
* Sets the destination of this unit.
*
* @param newDestination The new destination of this unit.
*/
public void setDestination(Location newDestination) {
this.destination = newDestination;
}
/**
* Finds a shortest path from the current <code>Tile</code> to the one
* specified. Only paths on water are allowed if <code>isNaval()</code>
* and only paths on land if not.
*
* The <code>Tile</code> at the <code>end</code> will not be checked
* against the legal moves of this <code>Unit</code>.
*
* @param end The <code>Tile</code> in which the path ends.
* @return A <code>PathNode</code> for the first tile in the path.
* @exception IllegalArgumentException if <code>end == null</code>
*/
public PathNode findPath(Tile end) {
if (getTile() == null) {
logger.warning("getTile() == null for " + toString()
+ " at location: " + getLocation());
}
return this.findPath(getTile(), end);
}
/**
* Finds a shortest path from the current <code>Tile</code> to the one
* specified. Only paths on water are allowed if <code>isNaval()</code>
* and only paths on land if not.
*
* @param start The <code>Tile</code> in which the path starts.
* @param end The <code>Tile</code> in which the path ends.
* @return A <code>PathNode</code> for the first tile in the path.
*/
public PathNode findPath(Tile start, Tile end) {
return this.findPath(start, end, null);
}
/**
* Finds a shortest path from the current <code>Tile</code> to the one
* specified. Only paths on water are allowed if <code>isNaval()</code>
* and only paths on land if not.
*
* @param start The <code>Tile</code> in which the path starts.
* @param end The <code>Tile</code> in which the path ends.
* @param carrier An optional <code>Unit</code> to carry the unit.
* @return A <code>PathNode</code> for the first tile in the path.
*/
public PathNode findPath(Tile start, Tile end, Unit carrier) {
return this.findPath(start, end, carrier, null);
}
/**
* Finds a shortest path from the current <code>Tile</code> to the one
* specified. Only paths on water are allowed if <code>isNaval()</code>
* and only paths on land if not.
*
* @param start The <code>Tile</code> in which the path starts from.
* @param end The <code>Tile</code> at the end of the path.
* @param carrier An optional <code>Unit</code> to carry the unit.
* @param costDecider An optional <code>CostDecider</code> for
* determining the movement costs (uses default cost deciders
* for the unit/s if not provided).
* @return A <code>PathNode</code> for the first tile in the path.
*/
public PathNode findPath(Tile start, Tile end, Unit carrier,
CostDecider costDecider) {
Location dest = getDestination();
setDestination(end);
PathNode path = getGame().getMap().findPath(this, start, end,
carrier, costDecider);
setDestination(dest);
return path;
}
/**
* Convenience wrapper to find a path to Europe for this unit.
*
* @return A path to Europe, or null if none found.
*/
public PathNode findPathToEurope() {
Location loc = getLocation();
return (loc instanceof Tile) ? this.findPathToEurope((Tile)loc) : null;
}
/**
* Convenience wrapper to find a path to Europe for this unit.
*
* @param start The <code>Tile</code> to start from.
* @return A path to Europe, or null if none found.
*/
public PathNode findPathToEurope(Tile start) {
return getGame().getMap().findPathToEurope(this, start, null);
}
/**
* Returns the number of turns this <code>Unit</code> will have to use in
* order to reach the given <code>Tile</code>.
*
* @param start The <code>Tile</code> to start the search from.
* @param end The <code>Tile</code> to be reached by this
* <code>Unit</code>.
* @return The number of turns it will take to reach the <code>end</code>,
* or <code>INFINITY</code> if no path can be found.
*/
public int getTurnsToReach(Tile start, Tile end) {
if (start == end) return 0;
PathNode p;
if (isOnCarrier()) {
Location dest = getDestination();
setDestination(end);
final Unit carrier = (Unit) getLocation();
p = this.findPath(start, end, carrier);
setDestination(dest);
} else {
p = this.findPath(start, end);
}
return (p != null) ? p.getTotalTurns() : INFINITY;
}
/**
* Returns the number of turns this <code>Unit</code> will have to
* use in order to reach the given <code>Location</code>.
*
* @param destination The destination for this unit.
* @return The number of turns it will take to reach the destination,
* or <code>INFINITY</code> if no path can be found.
*/
public int getTurnsToReach(Location destination) {
if (destination == null) {
logger.log(Level.WARNING, "destination == null", new Throwable());
}
Map map = getGame().getMap();
boolean toEurope = destination instanceof Europe;
Unit carrier = (isOnCarrier()) ? (Unit) getLocation() : null;
PathNode p;
// Handle the special cases of travelling to and from Europe
if (toEurope) {
if (isInEurope()) {
return 0;
} else if (isNaval()) {
p = this.findPathToEurope();
} else if (carrier != null) {
p = carrier.findPathToEurope();
} else {
return INFINITY;
}
return (p == null) ? INFINITY : p.getTotalTurns();
}
if (isInEurope()) {
if (isNaval()) {
p = this.findPath(getFullEntryLocation(),
destination.getTile());
} else {
if (carrier == null) {
// Pick a carrier. If none found the unit is stuck!
for (Unit u : getOwner().getUnits()) {
if (u.isNaval()) {
carrier = u;
break;
}
}
if (carrier == null) return INFINITY;
}
if (carrier.getFullEntryLocation().getTile()
== destination.getTile()) return carrier.getSailTurns();
p = this.findPath(carrier.getFullEntryLocation(),
destination.getTile(), carrier);
}
return (p == null) ? INFINITY
: p.getTotalTurns() + carrier.getSailTurns();
}
if (isAtSea()) {
if (isNaval()) {
p = this.findPath(getFullEntryLocation(),
destination.getTile());
carrier = this;
} else {
if (carrier == null) return INFINITY;
p = this.findPath(carrier.getFullEntryLocation(),
destination.getTile(), carrier);
}
return (p == null) ? INFINITY : p.getTotalTurns()
+ carrier.getWorkLeft();
}
// Not in Europe, at sea, or going to Europe, so there must be
// a well defined start and end tile.
Tile start = (carrier == null) ? getTile() : carrier.getTile();
return getTurnsToReach(start, destination.getTile());
}
/**
* Convenience wrapper for the
* {@link net.sf.freecol.common.model.Map#search} function.
*
* @param startTile The <code>Tile</code> to start the search from.
* @param gd The object responsible for determining whether a
* given <code>PathNode</code> is a goal or not.
* @param costDecider An optional <code>CostDecider</code>
* responsible for determining the path cost.
* @param maxTurns The maximum number of turns the given
* <code>Unit</code> is allowed to move.
* @param carrier The carrier the <code>unit</code> is currently
* onboard or <code>null</code> if the <code>unit</code> is
* either not onboard a carrier or should not use the
* carrier while finding the path.
* @return The path to a goal determined by the given
* <code>GoalDecider</code>.
*/
public PathNode search(Tile start, GoalDecider gd, CostDecider cd,
int maxTurns, Unit carrier) {
return getGame().getMap().search(this, start, gd, cd, maxTurns, carrier);
}
/**
* Gets the cost of moving this <code>Unit</code> onto the given
* <code>Tile</code>. A call to {@link #getMoveType(Tile)} will return
* <code>MOVE_NO_MOVES</code>, if {@link #getMoveCost} returns a move cost
* larger than the {@link #getMovesLeft moves left}.
*
* @param target The <code>Tile</code> this <code>Unit</code> will move
* onto.
* @return The cost of moving this unit onto the given <code>Tile</code>.
*/
public int getMoveCost(Tile target) {
return getMoveCost(getTile(), target, getMovesLeft());
}
/**
* Gets the cost of moving this <code>Unit</code> from the given
* <code>Tile</code> onto the given <code>Tile</code>. A call to
* {@link #getMoveType(Tile, Tile, int)} will return
* <code>MOVE_NO_MOVES</code>, if {@link #getMoveCost} returns a move cost
* larger than the {@link #getMovesLeft moves left}.
*
* @param from The <code>Tile</code> this <code>Unit</code> will move
* from.
* @param target The <code>Tile</code> this <code>Unit</code> 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</code>.
*/
public int getMoveCost(Tile from, Tile target, int ml) {
// Remember to also change map.findPath(...) if you change anything
// here.
// TODO: also pass direction, so that we can check for rivers
int cost = target.getType().getBasicMoveCost();
if (target.isLand()) {
TileItemContainer container = target.getTileItemContainer();
if (container != null) {
cost = container.getMoveCost(cost, from);
}
}
if (isNaval() && from.isLand()
&& from.getSettlement() == null) {
// 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.getSettlement()!=null) && ml != 0) {
cost = ml;
}
}
return cost;
}
/**
* Gets the type of a move made in a specified direction.
*
* @param direction The <code>Direction</code> of the move.
* @return The move type.
*/
public MoveType getMoveType(Direction direction) {
Tile tile = getTile();
if (tile == null) {
throw new IllegalStateException("getTile() == null, location is "
+ location);
}
Tile target = tile.getNeighbourOrNull(direction);
return (target == 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</code> of the move.
* @return The move type.
*/
public MoveType getMoveType(Tile target) {
return 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</code> of the move.
* @param target The target <code>Tile</code> 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;
}
}
}
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</code> of the move.
* @param target The target <code>Tile</code> 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</code> of the move.
* @return The move type, which will be one of the extended illegal move
* types on failure.
*/
public MoveType getSimpleMoveType(Tile target) {
Tile tile = getTile();
if (tile == null) {
throw new IllegalStateException("Null tile");
}
return getSimpleMoveType(tile, 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 tile = getTile();
if (tile == null) {
throw new IllegalStateException("Null tile");
}
Tile target = tile.getNeighbourOrNull(direction);
return getSimpleMoveType(tile, 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<code> of the move.
* @param target The target <code>Tile</code> of the move.
* @return The move type.
*/
private MoveType getNavalMoveType(Tile from, Tile target) {
if (target == null) {
return (getOwner().canMoveToEurope()) ? MoveType.MOVE_HIGH_SEAS
: MoveType.MOVE_NO_EUROPE;
} else if (isUnderRepair()) {
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 if (target.canMoveToEurope()
&& getOwner().canMoveToEurope()) {
return MoveType.MOVE_HIGH_SEAS;
} else {
return 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</code> of the move.
* @param target The target <code>Tile</code> 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, see:
// server/control/InGameInputHandler.java:move()
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()) {
switch (getRole()) {
case DEFAULT: case PIONEER:
return getLearnMoveType(from, settlement);
case MISSIONARY:
return getMissionaryMoveType(from, settlement);
case SCOUT:
return getScoutMoveType(from, settlement);
case SOLDIER: case DRAGOON:
return (allowMoveFrom(from))
? MoveType.ATTACK_SETTLEMENT
: MoveType.MOVE_NO_ATTACK_MARINE;
}
return MoveType.MOVE_ILLEGAL; // should not happen
} 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
if (defender == null || !getOwner().owns(defender)) {
return MoveType.MOVE_NO_ACCESS_EMBARK;
}
for (Unit unit : target.getUnitList()) {
if (unit.getSpaceLeft() >= getSpaceTaken()) {
return MoveType.EMBARK;
}
}
return MoveType.MOVE_NO_ACCESS_FULL;
}
}
/**
* Get the <code>MoveType</code> when moving a trading unit to a
* settlement.
*
* @param settlement The <code>Settlement</code> to move to.
* @return The appropriate <code>MoveType</code>.
*/
private MoveType getTradeMoveType(Settlement settlement) {
if (settlement instanceof Colony) {
return (getOwner().atWarWith(settlement.getOwner()))
? MoveType.MOVE_NO_ACCESS_WAR
: (!hasAbility("model.ability.tradeWithForeignColonies"))
? MoveType.MOVE_NO_ACCESS_TRADE
: MoveType.ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS;
} else if (settlement instanceof IndianSettlement) {
// Do not block for war, bringing gifts is allowed
return (!allowContact(settlement))
? MoveType.MOVE_NO_ACCESS_CONTACT
: (goodsContainer.getGoodsCount() > 0)
? MoveType.ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS
: MoveType.MOVE_NO_ACCESS_GOODS;
} else {
return MoveType.MOVE_ILLEGAL; // should not happen
}
}
/**
* Get the <code>MoveType</code> when moving a colonist to a settlement.
*
* @param from The <code>Tile</code> to move from.
* @param settlement The <code>Settlement</code> to move to.
* @return The appropriate <code>MoveType</code>.
*/
private MoveType getLearnMoveType(Tile from, Settlement settlement) {
if (settlement instanceof Colony) {
return MoveType.MOVE_NO_ACCESS_SETTLEMENT;
} else if (settlement instanceof IndianSettlement) {
UnitType scoutSkill = getSpecification()
.getUnitType("model.unit.seasonedScout");
return (!allowContact(settlement))
? MoveType.MOVE_NO_ACCESS_CONTACT
: (!allowMoveFrom(from))
? MoveType.MOVE_NO_ACCESS_WATER
: (!getType().canBeUpgraded(null, ChangeType.NATIVES))
? MoveType.MOVE_NO_ACCESS_SKILL
: MoveType.ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST;
} else {
return MoveType.MOVE_ILLEGAL; // should not happen
}
}
/**
* Get the <code>MoveType</code> when moving a missionary to a settlement.
*
* @param from The <code>Tile</code> to move from.
* @param settlement The <code>Settlement</code> to move to.
* @return The appropriate <code>MoveType</code>.
*/
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
: MoveType.ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY;
} else {
return MoveType.MOVE_ILLEGAL; // should not happen
}
}
/**
* Get the <code>MoveType</code> when moving a scout to a settlement.
*
* @param from The <code>Tile</code> to move from.
* @param settlement The <code>Settlement</code> to move to.
* @return The appropriate <code>MoveType</code>.
*/
private MoveType getScoutMoveType(Tile from, Settlement settlement) {
if (settlement instanceof Colony) {
// No allowMoveFrom check for Colonies
return MoveType.ENTER_FOREIGN_COLONY_WITH_SCOUT;
} else if (settlement instanceof IndianSettlement) {
return (!allowMoveFrom(from))
? MoveType.MOVE_NO_ACCESS_WATER
: MoveType.ENTER_INDIAN_SETTLEMENT_WITH_SCOUT;
} 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</code> to consider.
* @return True if the move is allowed.
*/
private boolean allowMoveFrom(Tile from) {
return from.isLand() || getSpecification()
.getBooleanOption("model.option.amphibiousMoves").getValue();
}
/**
* Is this unit allowed to contact a settlement?
*
* @param settlement The <code>Settlement</code> to consider.
* @return True if the contact is allowed.
*/
private boolean allowContact(Settlement settlement) {
return getOwner().hasContacted(settlement.getOwner());
}
/**
* Returns the amount of moves this Unit has left.
*
* @return The amount of moves this Unit has left.
*/
public int getMovesLeft() {
return movesLeft;
}
/**
* Sets the <code>movesLeft</code>.
*
* @param movesLeft The new amount of moves left this <code>Unit</code>
* should have. If <code>movesLeft < 0</code> then
* <code>movesLeft = 0</code>.
*/
public void setMovesLeft(int movesLeft) {
if (movesLeft < 0) {
movesLeft = 0;
}
this.movesLeft = movesLeft;
}
/**
* Gets the amount of space this <code>Unit</code> takes when put on a
* carrier.
*
* @return The space this <code>Unit</code> takes.
*/
public int getSpaceTaken() {
int space = unitType.getSpaceTaken() + getGoodsCount();
for (Unit u : units) space += u.getSpaceTaken();
return space;
}
/**
* Gets the line of sight of this <code>Unit</code>. That is the distance
* this <code>Unit</code> can spot new tiles, enemy unit e.t.c.
*
* @return The line of sight of this <code>Unit</code>.
*/
public int getLineOfSight() {
float line = unitType.getLineOfSight();
Set<Modifier> modifierSet = getModifierSet("model.modifier.lineOfSightBonus");
if (getTile() != null && getTile().getType() != null) {
modifierSet.addAll(getTile().getType().getFeatureContainer()
.getModifierSet("model.modifier.lineOfSightBonus",
unitType, getGame().getTurn()));
}
return (int) FeatureContainer.applyModifierSet(line, getGame().getTurn(), modifierSet);
}
/**
* Verifies if the unit is aboard a carrier
*/
public boolean isOnCarrier(){
return(this.getLocation() instanceof Unit);
}
/**
* Sets the given state to all the units that si beeing carried.
*
* @param state The state.
*/
public void setStateToAllChildren(UnitState state) {
if (canCarryUnits()) {
for (Unit u : getUnitList()) u.setState(state);
}
}
/**
* 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);
}
/**
* Returns true if this Unit could still be moved. This is used to
* prevent an accidental end of turn order.
*
* @return True if this unit could still be moved by the player.
*/
public boolean couldMove() {
return getState() == UnitState.ACTIVE
&& getMovesLeft() > 0
&& destination == null // Can not reach next tile
&& tradeRoute == null
&& !isUnderRepair()
&& !isAtSea()
&& !(isInEurope() && isOnCarrier())
// this should never happen anyway, since these units
// should have state IN_COLONY, but better safe than sorry
&& !(location instanceof WorkLocation);
}
/**
* Finds the closest <code>Location</code> to this tile where
* this ship can be repaired.
*
* @return The closest <code>Location</code> where a ship can be repaired.
*/
public Location getRepairLocation() {
Location closestLocation = null;
int shortestDistance = INFINITY;
Player player = getOwner();
Tile tile = getTile();
for (Colony colony : player.getColonies()) {
if (colony == null || colony == tile.getColony()) continue;
int distance;
if (colony.hasAbility("model.ability.repairUnits")) {
// Tile.getDistanceTo(Tile) doesn't care about
// connectivity, so we need to check for an available
// path to target colony instead
PathNode pn = this.findPath(colony.getTile());
if (pn != null
&& (distance = pn.getTotalTurns()) < shortestDistance) {
closestLocation = colony;
shortestDistance = distance;
}
}
}
boolean connected = tile.isConnected()
|| (tile.getColony() != null && tile.getColony().isConnected());
return (closestLocation != null) ? closestLocation.getTile()
: (connected) ? player.getEurope()
: null;
}
/**
* Adds a locatable to this <code>Unit</code>.
*
* @param locatable The <code>Locatable</code> to add to this
* <code>Unit</code>.
*/
public boolean add(Locatable locatable) {
if (locatable instanceof Unit) {
if (!canCarryUnits()) {
throw new IllegalStateException("Can not carry units: "
+ this.toString());
}
Unit unit = (Unit) locatable;
if (getSpaceLeft() < unit.getSpaceTaken()) {
throw new IllegalStateException("Not enough space for "
+ unit.toString()
+ " on " + this.toString());
}
if (units.contains(locatable)) {
logger.warning("Already on carrier: " + unit.toString());
return true;
}
if (units.equals(Collections.emptyList())) {
units = new ArrayList<Unit>();
}
spendAllMoves();
unit.setState(UnitState.SENTRY);
return units.add(unit);
} else if (locatable instanceof Goods) {
if (!canCarryGoods()) {
throw new IllegalStateException("Can not carry goods: "
+ this.toString());
}
Goods goods = (Goods) locatable;
if (getLoadableAmount(goods.getType()) < goods.getAmount()){
throw new IllegalStateException("Not enough space for "
+ goods.toString()
+ " on " + this.toString());
}
spendAllMoves();
return goodsContainer.addGoods(goods);
} else {
throw new IllegalStateException("Can not be added to unit: "
+ ((FreeColGameObject) locatable).toString());
}
}
/**
* Removes a <code>Locatable</code> from this <code>Unit</code>.
*
* @param locatable The <code>Locatable</code> to remove from this
* <code>Unit</code>.
*/
public boolean remove(Locatable locatable) {
if (locatable == null) {
throw new IllegalArgumentException("Locatable must not be 'null'.");
} else if (locatable instanceof Unit && canCarryUnits()) {
spendAllMoves();
return units.remove(locatable);
} else if (locatable instanceof Goods && canCarryGoods()) {
spendAllMoves();
return goodsContainer.removeGoods((Goods) locatable) != null;
} else {
logger.warning("Tried to remove a 'Locatable' from a non-carrier unit.");
}
return false;
}
/**
* Checks if this <code>Unit</code> contains the specified
* <code>Locatable</code>.
*
* @param locatable The <code>Locatable</code> to test the presence of.
* @return
* <ul>
* <li><i>true</i> if the specified <code>Locatable</code> is
* on this <code>Unit</code> and
* <li><i>false</i> otherwise.
* </ul>
*/
public boolean contains(Locatable locatable) {
if (locatable instanceof Unit && canCarryUnits()) {
return units.contains(locatable);
} else if (locatable instanceof Goods && canCarryGoods()) {
return goodsContainer.contains((Goods) locatable);
} else {
return false;
}
}
/**
* Checks wether or not the specified locatable may be added to this
* <code>Unit</code>. The locatable cannot be added is this
* <code>Unit</code> if it is not a carrier or if there is no room left.
*
* @param locatable The <code>Locatable</code> to test the addabillity of.
* @return The result.
*/
public boolean canAdd(Locatable locatable) {
if (locatable == this) {
return false;
} else if (locatable instanceof Unit && canCarryUnits()) {
return getSpaceLeft() >= locatable.getSpaceTaken();
} else if (locatable instanceof Goods) {
Goods g = (Goods) locatable;
return (getLoadableAmount(g.getType()) >= g.getAmount());
} else {
return false;
}
}
/**
* Returns the amount of a GoodsType that could be loaded.
*
* @param type a <code>GoodsType</code> value
* @return an <code>int</code> value
*/
public int getLoadableAmount(GoodsType type) {
if (canCarryGoods()) {
int result = getSpaceLeft() * GoodsContainer.CARGO_SIZE;
int count = getGoodsContainer().getGoodsCount(type)
% GoodsContainer.CARGO_SIZE;
if (count > 0 && count < GoodsContainer.CARGO_SIZE) {
result += GoodsContainer.CARGO_SIZE - count;
}
return result;
} else {
return 0;
}
}
/**
* Gets the amount of Units at this Location.
*
* @return The amount of Units at this Location.
*/
public int getUnitCount() {
return units.size();
}
/**
* Gets the first <code>Unit</code> beeing carried by this
* <code>Unit</code>.
*
* @return The <code>Unit</code>.
*/
public Unit getFirstUnit() {
if (units.isEmpty()) {
return null;
} else {
return units.get(0);
}
}
/**
* Gets the last <code>Unit</code> beeing carried by this
* <code>Unit</code>.
*
* @return The <code>Unit</code>.
*/
public Unit getLastUnit() {
if (units.isEmpty()) {
return null;
} else {
return units.get(units.size() - 1);
}
}
/**
* Checks if this unit is visible to the given player.
*
* @param player The <code>Player</code>.
* @return <code>true</code> if this <code>Unit</code> is visible to the
* given <code>Player</code>.
*/
public boolean isVisibleTo(Player player) {
if(player == getOwner()){
return true;
}
Tile unitTile = getTile();
if(unitTile == null){
return false;
}
if(!player.canSee(unitTile)){
return false;
}
Settlement settlement = getSettlement();
if(settlement != null && !player.owns(settlement)){
return false;
}
if(isOnCarrier() && !player.owns((Unit) getLocation())){
return false;
}
return true;
}
/**
* Gets a <code>Iterator</code> of every <code>Unit</code> directly
* located on this <code>Location</code>.
*
* @return The <code>Iterator</code>.
*/
public Iterator<Unit> getUnitIterator() {
return new ArrayList<Unit>(units).iterator();
}
/**
* Gets a list of the units in this carrier unit.
*
* @return The list of units in this carrier unit.
*/
public List<Unit> getUnitList() {
return new ArrayList<Unit>(units);
}
/**
* Gets a <code>Iterator</code> of every <code>Unit</code> directly
* located on this <code>Location</code>.
*
* @return The <code>Iterator</code>.
*/
public Iterator<Goods> getGoodsIterator() {
if (canCarryGoods()) {
return goodsContainer.getGoodsIterator();
} else {
return EmptyIterator.getInstance();
}
}
/**
* Returns a <code>List</code> containing the goods carried by this unit.
* @return a <code>List</code> containing the goods carried by this unit.
*/
public List<Goods> getGoodsList() {
if (canCarryGoods()) {
return goodsContainer.getGoods();
} else {
return Collections.emptyList();
}
}
public GoodsContainer getGoodsContainer() {
return goodsContainer;
}
/**
* Sets the units location without updating any other variables
*
* @param newLocation The new Location
*/
public void setLocationNoUpdate(Location newLocation) {
location = newLocation;
}
/**
* Sets the location of this Unit.
*
* @param newLocation The new <code>Location</code>.
*/
public void setLocation(Location newLocation) {
Location oldLocation = location;
// If either the add or remove involves a colony, call the
// colony-specific routine...
Colony oldColony = (location instanceof WorkLocation)
? this.getColony() : null;
Colony newColony = (newLocation instanceof WorkLocation)
? newLocation.getColony() : null;
// However if the unit is moving within the same colony,
// do not call the colony-specific routines.
if (oldColony == newColony) oldColony = newColony = null;
boolean result = true;
if (location != null) {
result = (oldColony != null)
? oldColony.removeUnit(this)
: location.remove(this);
}
/*if (!result) return false;*/
location = newLocation;
// Explore the new location now to prevent dealing with tiles
// with null (unexplored) type.
getOwner().setExplored(this);
// It is possible to add a unit to a non-specific location
// within a colony by specifying the colony as the new
// location.
if (newLocation instanceof Colony) {
newColony = (Colony) newLocation;
location = newLocation = newColony.getWorkLocationFor(this);
}
if (newLocation != null) {
result = (newColony != null)
? newColony.addUnit(this, (WorkLocation) newLocation)
: newLocation.add(this);
}
return /*result*/;
}
/**
* Sets the <code>IndianSettlement</code> that owns this unit.
*
* @param indianSettlement The <code>IndianSettlement</code> that should
* now be owning this <code>Unit</code>.
*/
public void setIndianSettlement(IndianSettlement indianSettlement) {
if (this.indianSettlement != null) {
this.indianSettlement.removeOwnedUnit(this);
}
this.indianSettlement = indianSettlement;
if (indianSettlement != null) {
indianSettlement.addOwnedUnit(this);
}
}
/**
* Gets the <code>IndianSettlement</code> that owns this unit.
*
* @return The <code>IndianSettlement</code>.
*/
public IndianSettlement getIndianSettlement() {
return indianSettlement;
}
/**
* Gets the location of this Unit.
*
* @return The location of this Unit.
*/
public Location getLocation() {
return location;
}
/**
* Checks whether this unit can be equipped with the given
* <code>EquipmentType</code> at the current
* <code>Location</code>. This is the case if all requirements of
* the EquipmentType are met.
*
* @param equipmentType an <code>EquipmentType</code> value
* @return whether this unit can be equipped with the given
* <code>EquipmentType</code> at the current location.
*/
public boolean canBeEquippedWith(EquipmentType equipmentType) {
for (Entry<String, Boolean> entry : equipmentType.getUnitAbilitiesRequired().entrySet()) {
if (hasAbility(entry.getKey()) != entry.getValue()) {
return false;
}
}
if (equipment.getCount(equipmentType) >= equipmentType.getMaximumCount()) {
return false;
}
return true;
}
/**
* Changes the equipment a unit has and returns a list of equipment
* it still has but needs to drop due to the changed equipment being
* incompatible.
*
* @param type The <code>EquipmentType</code> to change.
* @param amount The amount to change by (may be negative).
* @return A list of equipment types that the unit must now drop.
*/
public List<EquipmentType> changeEquipment(EquipmentType type, int amount) {
List<EquipmentType> result = new ArrayList<EquipmentType>();
equipment.incrementCount(type, amount);
if (amount > 0) {
for (EquipmentType oldType
: new HashSet<EquipmentType>(equipment.keySet())) {
if (!oldType.isCompatibleWith(type)) {
result.add(oldType);
}
}
}
setRole();
return result;
}
/**
* Describe <code>getEquipmentCount</code> method here.
*
* @param equipmentType an <code>EquipmentType</code> value
* @return an <code>int</code> value
*/
public int getEquipmentCount(EquipmentType equipmentType) {
return equipment.getCount(equipmentType);
}
/**
* Checks if this <code>Unit</code> is located in Europe. That is; either
* directly or onboard a carrier which is in Europe.
*
* @return The result.
*/
public boolean isInEurope() {
if (location instanceof Unit) {
return ((Unit) location).isInEurope();
} else {
return getLocation() instanceof Europe;
}
}
/**
* Checks whether this <code>Unit</code> is at sea off the map, or
* on board of a carrier that is.
*
* @return The result.
*/
public boolean isAtSea() {
if (location instanceof Unit) {
return ((Unit) location).isAtSea();
} else {
return location instanceof HighSeas;
}
}
/**
* Checks if this <code>Unit</code> is able to carry {@link Locatable}s.
*
* @return 'true' if this unit can carry goods or other units,
* 'false' otherwise.
*/
public boolean isCarrier() {
return unitType.canCarryGoods() ||
unitType.canCarryUnits();
}
/**
* Checks if this unit is a person, that is not a ship or wagon.
* Surprisingly difficult without explicit enumeration because
* model.ability.person only arrived in 0.10.1.
*
* @return True if this unit is a person.
*/
public boolean isPerson() {
return hasAbility("model.ability.person")
// @compat 0.10.0
|| hasAbility(Ability.BORN_IN_COLONY)
|| hasAbility(Ability.BORN_IN_INDIAN_SETTLEMENT)
|| hasAbility("model.ability.foundColony")
// Nick also had:
// && (!hasAbility("model.ability.carryGoods")
// && !hasAbility("model.ability.carryUnits")
// && !hasAbility("model.ability.carryTreasure")
// && !hasAbility("model.ability.bombard"))
// ...but that should be unnecessary.
// end compatibility code
;
}
/**
* Gets the owner of this Unit.
*
* @return The owner of this Unit.
*/
public Player getOwner() {
return owner;
}
/**
* Get the name of the apparent owner of this Unit,
* (like getOwner().getNationAsString() but handles pirates)
*
* @return The name of the owner of this Unit unless this is hidden.
*/
public StringTemplate getApparentOwnerName() {
return ((hasAbility(Ability.PIRACY))
? getGame().getUnknownEnemy()
: owner).getNationName();
}
/**
* Sets the owner of this Unit.
*
* @param owner The new owner of this Unit.
*/
public void setOwner(Player owner) {
Player oldOwner = this.owner;
// safeguard
if (oldOwner == owner) {
return;
} else if (oldOwner == null) {
logger.warning("Unit " + getId() + " had no previous owner, when changing owner to " + owner.getId());
}
// Clear trade route and goto orders if changing owner.
if (getTradeRoute() != null) {
setTradeRoute(null);
}
if (getDestination() != null) {
setDestination(null);
}
// This need to be set right away
this.owner = owner;
// If its a carrier, we need to update the units it has loaded
//before finishing with it
for (Unit unit : getUnitList()) {
unit.setOwner(owner);
}
if(oldOwner != null) {
oldOwner.removeUnit(this);
oldOwner.modifyScore(-getType().getScoreValue());
// for speed optimizations
if(!isOnCarrier()){
oldOwner.invalidateCanSeeTiles();
}
}
owner.setUnit(this);
if(getType() != null) { // can be null if setOwner() is called from fixIntegrity()
owner.modifyScore(getType().getScoreValue());
}
// for speed optimizations
if(!isOnCarrier()) {
getOwner().setExplored(this);
}
if (getGame().getFreeColGameObjectListener() != null) {
getGame().getFreeColGameObjectListener().ownerChanged(this, oldOwner, owner);
}
}
/**
* 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 String getNationality() {
return 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 void setNationality(String newNationality) {
if (isPerson()) {
nationality = newNationality;
} else {
throw new UnsupportedOperationException("Can not set the nationality of a Unit which is not a person!");
}
}
/**
* 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 String getEthnicity() {
return ethnicity;
}
/**
* Sets the ethnicity of this Unit.
* Ethnicity is something units are born with. It cannot be subsequently changed.
*
* @param newEthnicity The new ethnicity of this Unit.
*/
public void setEthnicity(String newEthnicity) {
throw new UnsupportedOperationException("Can not change a Unit's ethnicity!");
}
/**
* 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();
// return getGame().getSpecification().getNationType(ethnicity).hasAbility("model.ability.native");
// return getGame().getSpecification().getIndianNationTypes().contains(getNationType(ethnicity));
} catch (Exception e) {
return false;
}
}
/**
* Sets the type of the unit.
*
* @param newUnitType The new type of the unit.
*/
public void setType(UnitType newUnitType) {
if (newUnitType.isAvailableTo(owner)) {
if (unitType == null) {
owner.modifyScore(newUnitType.getScoreValue());
} else {
owner.modifyScore(newUnitType.getScoreValue() - unitType.getScoreValue());
}
this.unitType = newUnitType;
if (getMovesLeft() > getInitialMovesLeft()) {
setMovesLeft(getInitialMovesLeft());
}
hitpoints = unitType.getHitPoints();
if (getTeacher() != null && !canBeStudent(getTeacher())) {
getTeacher().setStudent(null);
setTeacher(null);
}
} else {
// ColonialRegulars only available after independence is declared
logger.warning("Units of type: " + newUnitType
+ " are not available to " + owner.getPlayerType()
+ " player " + owner.getName());
}
}
// TODO: make these go away, if possible, private if not
public boolean isArmed() {
if(getOwner().isIndian()){
return equipment.containsKey(getSpecification().getEquipmentType("model.equipment.indian.muskets"));
}
return equipment.containsKey(getSpecification().getEquipmentType("model.equipment.muskets"));
}
public boolean isMounted() {
if(getOwner().isIndian()){
return equipment.containsKey(getSpecification().getEquipmentType("model.equipment.indian.horses"));
}
return equipment.containsKey(getSpecification().getEquipmentType("model.equipment.horses"));
}
/**
* Describe <code>getName</code> method here.
*
* @return a <code>String</code> value
*/
public String getName() {
return name;
}
/**
* Set the <code>Name</code> value.
*
* @param newName The new Name value.
*/
public void setName(String newName) {
this.name = newName;
}
/**
* Describe <code>getLabel</code> method here.
*
* @return a <code>StringTemplate</code> value
*/
public StringTemplate getLabel() {
StringTemplate result = StringTemplate.label(" ")
.add(getType().getNameKey());
if (name != null) {
result.addName(name);
}
Role role = getRole();
if (role != Role.DEFAULT) {
result = StringTemplate.template("model.unit." + role.getId()
+ ".name")
.addAmount("%number%", 1)
.add("%unit%", getType().getNameKey());
}
return result;
}
/**
* Return a description of the unit's equipment.
*
* @return a <code>String</code> value
*/
public StringTemplate getEquipmentLabel() {
if (equipment != null && !equipment.isEmpty()) {
StringTemplate result = StringTemplate.label("/");
for (java.util.Map.Entry<EquipmentType, Integer> entry : equipment.getValues().entrySet()) {
EquipmentType type = entry.getKey();
int amount = entry.getValue().intValue();
if (type.getGoodsRequired().isEmpty()) {
result.addStringTemplate(StringTemplate.template("model.goods.goodsAmount")
.add("%goods%", type.getNameKey())
.addName("%amount%", Integer.toString(amount)));
} else {
for (AbstractGoods goods : type.getGoodsRequired()) {
result.addStringTemplate(StringTemplate.template("model.goods.goodsAmount")
.add("%goods%", goods.getType().getNameKey())
.addName("%amount%", Integer.toString(amount * goods.getAmount())));
}
}
}
return result;
} else {
return null;
}
}
/**
* 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.
*/
public int getInitialMovesLeft() {
return (int) FeatureContainer.applyModifierSet(unitType.getMovement(), getGame().getTurn(),
getModifierSet("model.modifier.movementBonus"));
}
/**
* Sets the hitpoints for this unit.
*
* @param hitpoints The hitpoints this unit has. This is currently only used
* for damaged ships, but might get an extended use later.
* @see UnitType#getHitPoints
*/
public void setHitpoints(int hitpoints) {
this.hitpoints = hitpoints;
if (hitpoints >= unitType.getHitPoints()) {
setState(UnitState.ACTIVE);
}
}
/**
* Returns the hitpoints.
*
* @return The hitpoints this unit has. This is currently only used for
* damaged ships, but might get an extended use later.
* @see UnitType#getHitPoints
*/
public int getHitpoints() {
return hitpoints;
}
/**
* Checks if this unit is under repair.
*
* @return <i>true</i> if under repair and <i>false</i> otherwise.
*/
public boolean isUnderRepair() {
return (hitpoints < unitType.getHitPoints());
}
/**
* Checks if this unit is running a mission.
*
* @return True if this unit is running a mission.
*/
public boolean isInMission() {
return getRole() == Role.MISSIONARY
&& getTile() == null
&& !(getLocation() instanceof Unit);
}
/**
* Returns a String representation of this Unit.
*
* @return A String representation of this Unit.
*/
public String toString() {
return getId() + " [" + getType().getId() + " " + getMovesAsString() +"] "
+ owner.getNationID() + " (" + getRole() + ")";
}
public String getMovesAsString() {
String moves = "";
int quotient = getMovesLeft() / 3;
int remainder = getMovesLeft() % 3;
if (remainder == 0 || quotient > 0) {
moves += Integer.toString(quotient);
}
if (remainder > 0) {
if (quotient > 0) {
moves += " ";
}
moves += "(" + Integer.toString(remainder) + "/3) ";
}
moves += "/" + Integer.toString(getInitialMovesLeft() / 3);
return moves;
}
/**
* Checks if this <code>Unit</code> is naval.
*
* @return <i>true</i> if this Unit is a naval Unit and <i>false</i>
* otherwise.
*/
public boolean isNaval() {
return getType().isNaval();
}
/**
* Gets the state of this <code>Unit</code>.
*
* @return The state of this <code>Unit</code>.
*/
public UnitState getState() {
return state;
}
/**
* Gets the <code>Role</code> of this <code>Unit</code>.
*
* @return The <code>role</code> of this <code>Unit</code>.
*/
public Role getRole() {
return role;
}
/**
* Determine role based on equipment.
*/
protected void setRole() {
Role oldRole = role;
role = Role.DEFAULT;
for (EquipmentType type : equipment.keySet()) {
role = role.newRole(type.getRole());
}
if (getState() == UnitState.IMPROVING && role != Role.PIONEER) {
setStateUnchecked(UnitState.ACTIVE);
setMovesLeft(0);
}
// Check for role change for reseting the experience.
// Soldier and Dragoon are compatible, no loss of experience.
if (!role.isCompatibleWith(oldRole)) {
experience = 0;
}
}
/**
* Checks if a <code>Unit</code> 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 Unit's state can be changed to the new value,
* 'false' otherwise.
*/
public boolean checkSetState(UnitState s) {
switch (s) {
case ACTIVE:
case SENTRY:
return true;
case IN_COLONY:
return !isNaval();
case FORTIFIED:
return getState() == UnitState.FORTIFYING;
case IMPROVING:
if (location instanceof Tile
&& getOwner().canAcquireForImprovement(location.getTile())) {
return getMovesLeft() > 0;
}
return false;
case SKIPPED:
if (getState() == UnitState.ACTIVE) return true;
// Fall through
case FORTIFYING:
return (getMovesLeft() > 0);
default:
logger.warning("Invalid unit state: " + s);
return false;
}
}
/**
* 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() {
return (int) getOwner().getFeatureContainer()
.applyModifier(getSpecification()
.getIntegerOption("model.option.turnsToSail")
.getValue(),
"model.modifier.sailHighSeas",
unitType, getGame().getTurn());
}
/**
* 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);
}
}
protected void setStateUnchecked(UnitState s) {
// TODO: 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) {
if (!workImprovement.isComplete()
&& workImprovement.getTile() != null
&& workImprovement.getTile().getTileItemContainer() != null) {
workImprovement.getTile().getTileItemContainer().removeTileItem(workImprovement);
}
setWorkImprovement(null);
}
break;
default:
// do nothing
break;
}
// Now initiate the new UnitState
switch (s) {
case ACTIVE:
setWorkLeft(-1);
break;
case SENTRY:
setWorkLeft(-1);
break;
case FORTIFIED:
setWorkLeft(-1);
movesLeft = 0;
break;
case FORTIFYING:
setWorkLeft(1);
movesLeft = 0;
break;
case IMPROVING:
if (workImprovement == null) {
setWorkLeft(-1);
} else {
setWorkLeft(workImprovement.getTurnsToComplete());
}
movesLeft = 0;
break;
case SKIPPED: // do nothing
break;
default:
setWorkLeft(-1);
}
state = s;
}
/**
* Checks if this <code>Unit</code> can be moved to Europe.
*
* TODO: the new Carribean map has no south pole, and this allows
* moving to Europe via the bottom edge of the map, which is
* approximately the equator line. Should we enforce moving to
* Europe requires high seas, and no movement via north/south
* poles?
*
* mpope 201103: Just leave it up to the map itself, tiles can
* include a "moveToEurope" attribute to override the default
* behaviour, which is to allow movement to Europe from tiles with
* the moveToEurope ability or on the map borders.
*
* Now, IMHO on the Carribean map, settling on the land next to
* the south border gives an unfair advantage and we *should* set
* moveToEurope==false on the nearby sea tiles.
*
* @return <code>true</code> if this unit can move to Europe.
*/
public boolean canMoveToEurope() {
if (getLocation() instanceof Europe) {
return true;
}
if (!getOwner().canMoveToEurope()) {
return false;
}
for (Tile tile : getTile().getSurroundingTiles(1)) {
if (tile.canMoveToEurope()) return true;
}
return 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</code> if this unit can build a colony.
*/
public boolean canBuildColony() {
return unitType.hasAbility("model.ability.foundColony")
&& getMovesLeft() > 0
&& getTile() != null;
}
/**
* Returns the Tile where this Unit is located. Or null if its location is
* Europe.
*
* @return The Tile where this Unit is located. Or null if its location is
* Europe.
*/
public Tile getTile() {
return (location != null) ? location.getTile() : null;
}
/**
* Returns the amount of space left on this Unit.
*
* @return The amount of units/goods than can be moved onto this Unit.
*/
public int getSpaceLeft() {
int space = unitType.getSpace() - getGoodsCount();
Iterator<Unit> unitIterator = getUnitIterator();
while (unitIterator.hasNext()) {
Unit u = unitIterator.next();
space -= u.getSpaceTaken();
}
return space;
}
/**
* Returns the amount of goods that is carried by this unit.
*
* @return The amount of goods carried by this <code>Unit</code>. This
* value might different from the one returned by
* {@link #getGoodsCount()} when the model is
* {@link Game#getViewOwner() owned by a client} and cargo hiding
* has been enabled.
*/
public int getVisibleGoodsCount() {
if (visibleGoodsCount >= 0) {
return visibleGoodsCount;
} else {
return getGoodsCount();
}
}
public int getGoodsCount() {
return canCarryGoods() ? goodsContainer.getGoodsCount() : 0;
}
/**
* Move the given unit to the front of this carrier (make sure it'll be the
* first unit in this unit's unit list).
*
* @param u The unit to move to the front.
*/
public void moveToFront(Unit u) {
if (canCarryUnits() && units.remove(u)) {
units.add(0, u);
}
}
/**
* 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
&& unitType.hasAbility(Ability.EXPERT_PIONEER))
? (getWorkLeft() + 1) / 2
: getWorkLeft();
}
/**
* Sets the entry location in which this unit will be put when
* returning from {@link Europe}.
*
* @param entryLocation The entry location.
* @see #getEntryLocation
*/
public void setEntryLocation(Location entryLocation) {
this.entryLocation = entryLocation;
if (entryLocation != null) {
owner.setEntryLocation(entryLocation);
}
}
/**
* Gets the entry location for this unit to use when returning from
* {@link Europe}.
*
* @return The entry location.
*/
public Location getEntryLocation() {
if (entryLocation == null) {
entryLocation = owner.getEntryLocation();
}
return entryLocation;
}
/**
* Gets the entry tile for this unit, or if null the default
* entry location for the owning player.
*
* @return The entry tile.
*/
public Tile getFullEntryLocation() {
return (entryLocation != null) ? (Tile) entryLocation
: (owner.getEntryLocation() == null) ? null
: owner.getEntryLocation().getTile();
}
/**
* Checks if this is an defensive unit. That is: a unit which can be used to
* defend a <code>Settlement</code>.
*
* <br><br>
*
* 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.
*
* <br><br>
*
* A settlement is lost when there are no more defensive units.
*
* @return <code>true</code> if this is a defensive unit meaning it can be
* used to defend a <code>Colony</code>. This would normally mean
* that a defensive unit also will be
* {@link #isOffensiveUnit offensive}.
*/
public boolean isDefensiveUnit() {
return (unitType.getDefence() > UnitType.DEFAULT_DEFENCE || isArmed() || isMounted()) && !isNaval();
}
/**
* 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</code>.
* @param defenderPower Its defence power.
* @param other An alternate <code>Unit</code>.
* @param otherPower Its defence power.
* @return True if the other unit should be preferred.
*/
public static boolean betterDefender(Unit defender, float defenderPower,
Unit other, float otherPower) {
if (defender == null) {
return true;
} else if (!defender.isArmed() && other.isArmed()) {
return true;
} else if (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;
}
}
/**
* Checks if this unit is an undead.
* @return return true if the unit is undead
*/
public boolean isUndead() {
return hasAbility("model.ability.undead");
}
/**
* Returns true if this unit can carry treasure (like a treasure train)
*
* @return <code>true</code> if this <code>Unit</code> is capable of
* carrying treasure.
*/
public boolean canCarryTreasure() {
return unitType.hasAbility(Ability.CARRY_TREASURE);
}
/**
* Returns true if this unit is a ship that can capture enemy goods.
*
* @return <code>true</code> if this <code>Unit</code> is capable of
* capturing goods.
*/
public boolean canCaptureGoods() {
return unitType.hasAbility(Ability.CAPTURE_GOODS);
}
/**
* After winning a battle, can this unit the loser equipment?
*
* @param equip The <code>EquipmentType</code> to consider.
* @param loser The loser <code>Unit</code>.
* @return The <code>EquipmentType</code> to capture, which may
* differ from the equip parameter due to transformations such
* as to the native versions of horses and muskets.
* Or return null if capture is not possible.
*/
public EquipmentType canCaptureEquipment(EquipmentType equip, Unit loser) {
if (hasAbility("model.ability.captureEquipment")) {
if (getOwner().isIndian() != loser.getOwner().isIndian()) {
equip = equip.getCaptureEquipment(getOwner().isIndian());
}
return (canBeEquippedWith(equip)) ? equip : null;
}
return null;
}
/**
* Gets the Settlement this unit is in.
*
* @return The Settlement this unit is in, or null if none.
*/
public Settlement getSettlement() {
Location location = getLocation();
return (location != null) ? location.getSettlement() : null;
}
/**
* Gets the Colony this unit is in.
*
* @return The Colony this unit is in, or null if none.
*/
public Colony getColony() {
Location location = getLocation();
return (location != null) ? location.getColony() : null;
}
/**
* Given a type of goods to produce in the field and a tile,
* returns the unit's potential to produce goods.
*
* @param goodsType The type of goods to be produced.
* @param base an <code>int</code> value
* @return The potential amount of goods to be farmed.
*/
// TODO: do we need this?
public int getProductionOf(GoodsType goodsType, int base) {
if (base == 0) {
return 0;
} else {
return Math.round(FeatureContainer.applyModifierSet(base, getGame().getTurn(),
getModifierSet(goodsType.getId())));
}
}
/**
* Removes all references to this object.
*
* @return A list of disposed objects.
*/
public List<FreeColGameObject> disposeList() {
List<FreeColGameObject> objects = new ArrayList<FreeColGameObject>();
while (units.size() > 0) {
objects.addAll(units.remove(0).disposeList());
}
if (location != null) {
location.remove(this);
}
if (teacher != null) {
teacher.setStudent(null);
teacher = null;
}
if (student != null) {
student.setTeacher(null);
student = null;
}
setIndianSettlement(null);
getOwner().invalidateCanSeeTiles();
getOwner().removeUnit(this);
if (unitType.canCarryGoods()) {
objects.addAll(goodsContainer.disposeList());
}
objects.addAll(super.disposeList());
return objects;
}
/**
* Removes all references to this object.
*/
public void dispose() {
disposeList();
}
/**
* Return how many turns left to be repaired
*
* @return turns to be repaired
*/
public int getTurnsForRepair() {
return unitType.getHitPoints() - getHitpoints();
}
/**
* Gets the available equipment that can be equipped automatically
* in case of an attack.
*
* @return The equipment that can be automatically equipped by
* this unit, or null if none.
*/
public TypeCountMap<EquipmentType> getAutomaticEquipment(){
// 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. Indians can also pick up equipment.
if(isArmed()){
return null;
}
if(!getOwner().hasAbility("model.ability.automaticEquipment")){
return null;
}
Settlement settlement = null;
if (getLocation() instanceof WorkLocation) {
settlement = getColony();
}
if (getLocation() instanceof IndianSettlement) {
settlement = (Settlement) getLocation();
}
if(settlement == null){
return null;
}
TypeCountMap<EquipmentType> equipmentList = null;
// Check for necessary equipment in the settlement
Set<Ability> autoDefence = getOwner().getFeatureContainer().getAbilitySet("model.ability.automaticEquipment");
for (EquipmentType equipment : getSpecification().getEquipmentTypeList()) {
for (Ability ability : autoDefence) {
if (!ability.appliesTo(equipment)){
continue;
}
if (!canBeEquippedWith(equipment)) {
continue;
}
boolean hasReqGoods = true;
for(AbstractGoods goods : equipment.getGoodsRequired()){
if(settlement.getGoodsCount(goods.getType()) < goods.getAmount()){
hasReqGoods = false;
break;
}
}
if(hasReqGoods){
// lazy initialization, required
if(equipmentList == null){
equipmentList = new TypeCountMap<EquipmentType>();
}
equipmentList.incrementCount(equipment, 1);
}
}
}
return equipmentList;
}
/**
* Does losing a piece of equipment mean the death of this unit?
*
* @param lose The <code>EquipmentType</code> to lose.
* @return True if the unit is doomed.
*/
public boolean losingEquipmentKillsUnit(EquipmentType lose) {
if (hasAbility("model.ability.disposeOnAllEquipLost")) {
for (EquipmentType equip : getEquipment().keySet()) {
if (equip != lose) return false;
}
return true;
}
return false;
}
/**
* Does losing a piece of equipment mean the demotion of this unit?
*
* @param lose The <code>EquipmentType</code> to lose.
* @return True if the unit is to be demoted.
*/
public boolean losingEquipmentDemotesUnit(EquipmentType lose) {
if (hasAbility("model.ability.demoteOnAllEquipLost")) {
for (EquipmentType equip : getEquipment().keySet()) {
if (equip != lose) return false;
}
return true;
}
return false;
}
/**
* Gets the probability that an attack by this unit will provoke a
* native to convert.
*
* @return A probability of conversion.
*/
public float getConvertProbability() {
Specification spec = getSpecification();
int opt = spec.getIntegerOption("model.option.nativeConvertProbability")
.getValue();
return 0.01f * FeatureContainer.applyModifierSet(opt,
getGame().getTurn(),
getModifierSet("model.modifier.nativeConvertBonus"));
}
/**
* Gets the probability that an attack by this unit will provoke natives
* to burn our missions.
*
* @return A probability of burning missions.
*/
public float getBurnProbability() {
// TODO: enhance burn probability proportionally with tension
return 0.01f * getSpecification()
.getIntegerOption("model.option.burnProbability").getValue();
}
/**
* Get a type change for this unit.
*
* @param change The <code>ChangeType</code> to consider.
* @param owner The <code>Player</code> to own this unit after a
* change of type CAPTURE or UNDEAD.
* @return The resulting unit type or null if there is no change suitable.
*/
public UnitType getTypeChange(ChangeType change, Player owner) {
return getType().getTargetType(change, owner);
}
/**
* Gets the best combat equipment type that this unit has.
*
* @param equipment The equipment to look through, such as returned by
* @see Unit#getEquipment() and/or @see Unit#getAutomaticEquipment().
* @return The equipment type to lose, or null if none.
*/
public EquipmentType getBestCombatEquipmentType(TypeCountMap<EquipmentType> equipment) {
EquipmentType lose = null;
if (equipment != null) {
int priority = -1;
for (EquipmentType equipmentType : equipment.keySet()) {
if (equipmentType.getCombatLossPriority() > priority) {
lose = equipmentType;
priority = equipmentType.getCombatLossPriority();
}
}
}
return lose;
}
// Interface Consumer
/**
* Returns a list of GoodsTypes this Consumer consumes.
*
* @return a <code>List</code> value
*/
public List<AbstractGoods> getConsumedGoods() {
return unitType.getConsumedGoods();
}
/**
* Describe <code>getProductionInfo</code> method here.
*
* @return a <code>ProductionInfo</code> value
*/
public ProductionInfo getProductionInfo(List<AbstractGoods> input) {
ProductionInfo result = new ProductionInfo();
result.setConsumption(getType().getConsumedGoods());
result.setMaximumConsumption(getType().getConsumedGoods());
return result;
}
/**
* The priority of this Consumer. The higher the priority, the
* earlier will the Consumer be allowed to consume the goods it
* requires.
*
* @return an <code>int</code> value
*/
public int getPriority() {
return unitType.getPriority();
}
// Serialization
private void unitsToXML(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
if (!units.isEmpty()) {
out.writeStartElement(UNITS_TAG_NAME);
for (Unit unit : units) {
unit.toXML(out, player, showAll, toSavedGame);
}
out.writeEndElement();
}
}
/**
* This method writes an XML-representation of this object to the given
* stream.
*
* <br>
* <br>
*
* Only attributes visible to the given <code>Player</code> will be added
* to that representation if <code>showAll</code> is set to
* <code>false</code>.
*
* @param out The target stream.
* @param player The <code>Player</code> this XML-representation should be
* made for, or <code>null</code> if
* <code>showAll == true</code>.
* @param showAll Only attributes visible to <code>player</code> will be
* added to the representation if <code>showAll</code> is set
* to <i>false</i>.
* @param toSavedGame If <code>true</code> then information that is only
* needed when saving a game is added.
* @throws XMLStreamException if there are any problems writing to the
* stream.
*/
protected void toXMLImpl(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
boolean full = showAll || toSavedGame || player == getOwner();
// Start element:
out.writeStartElement(getXMLElementTagName());
out.writeAttribute(ID_ATTRIBUTE, getId());
if (name != null) {
out.writeAttribute("name", name);
}
out.writeAttribute("unitType", unitType.getId());
out.writeAttribute("movesLeft", Integer.toString(movesLeft));
out.writeAttribute("state", state.toString());
out.writeAttribute("role", role.toString());
if (!full && hasAbility(Ability.PIRACY)) {
// Pirates do not disclose national characteristics.
out.writeAttribute("owner", getGame().getUnknownEnemy().getId());
} else {
out.writeAttribute("owner", getOwner().getId());
if (isPerson()) {
// Do not write out nationality and ethnicity for non-persons.
out.writeAttribute("nationality",
(nationality != null) ? nationality
: getOwner().getNationID());
out.writeAttribute("ethnicity",
(ethnicity != null) ? ethnicity
: getOwner().getNationID());
}
}
out.writeAttribute("turnsOfTraining", Integer.toString(turnsOfTraining));
if (workType != null) out.writeAttribute("workType", workType.getId());
if (experienceType != null) out.writeAttribute("experienceType",
experienceType.getId());
out.writeAttribute("experience", Integer.toString(experience));
out.writeAttribute("treasureAmount", Integer.toString(treasureAmount));
out.writeAttribute("hitpoints", Integer.toString(hitpoints));
out.writeAttribute("attrition", Integer.toString(attrition));
writeAttribute(out, "student", student);
writeAttribute(out, "teacher", teacher);
if (full) {
writeAttribute(out, "indianSettlement", indianSettlement);
out.writeAttribute("workLeft", Integer.toString(workLeft));
} else {
out.writeAttribute("workLeft", Integer.toString(-1));
}
if (entryLocation != null) {
out.writeAttribute("entryLocation", entryLocation.getId());
}
if (location != null) {
if (full || !(location instanceof Building
|| location instanceof ColonyTile)) {
out.writeAttribute("location", location.getId());
} else {
out.writeAttribute("location", getColony().getId());
}
}
if (destination != null) {
out.writeAttribute("destination", destination.getId());
}
if (tradeRoute != null) {
out.writeAttribute("tradeRoute", tradeRoute.getId());
out.writeAttribute("currentStop", String.valueOf(currentStop));
}
if (workImprovement != null) {
workImprovement.toXML(out, player, showAll, toSavedGame);
}
// Do not show enemy units hidden in a carrier:
if (full) {
unitsToXML(out, player, showAll, toSavedGame);
if (getType().canCarryGoods()) {
goodsContainer.toXML(out, player, showAll, toSavedGame);
}
} else {
if (getType().canCarryGoods()) {
out.writeAttribute("visibleGoodsCount", Integer.toString(getGoodsCount()));
goodsContainer.toXML(out, player, showAll, toSavedGame);
}
}
if (!equipment.isEmpty()) {
for (Entry<EquipmentType, Integer> entry : equipment.getValues().entrySet()) {
out.writeStartElement(EQUIPMENT_TAG);
out.writeAttribute(ID_ATTRIBUTE_TAG, entry.getKey().getId());
out.writeAttribute("count", entry.getValue().toString());
out.writeEndElement();
}
}
out.writeEndElement();
}
/**
* Initialize this object from an XML-representation of this object.
*
* @param in The input stream with the XML.
* @throws org.freecolandroid.xml.stream.XMLStreamException is thrown if something goes wrong.
*/
protected void readFromXMLImpl(XMLStreamReader in) throws XMLStreamException {
setId(in.getAttributeValue(null, ID_ATTRIBUTE));
setName(in.getAttributeValue(null, "name"));
UnitType oldUnitType = unitType;
unitType = getSpecification().getUnitType(in.getAttributeValue(null, "unitType"));
movesLeft = Integer.parseInt(in.getAttributeValue(null, "movesLeft"));
state = Enum.valueOf(UnitState.class, in.getAttributeValue(null, "state"));
role = Enum.valueOf(Role.class, in.getAttributeValue(null, "role"));
workLeft = Integer.parseInt(in.getAttributeValue(null, "workLeft"));
attrition = getAttribute(in, "attrition", 0);
String ownerId = in.getAttributeValue(null, "owner");
owner = (Player) getGame().getFreeColGameObject(ownerId);
if (owner == null) owner = new Player(getGame(), ownerId);
nationality = in.getAttributeValue(null, "nationality");
ethnicity = in.getAttributeValue(null, "ethnicity");
if (oldUnitType == null) {
owner.modifyScore(unitType.getScoreValue());
} else {
owner.modifyScore(unitType.getScoreValue() - oldUnitType.getScoreValue());
}
turnsOfTraining = Integer.parseInt(in.getAttributeValue(null, "turnsOfTraining"));
hitpoints = Integer.parseInt(in.getAttributeValue(null, "hitpoints"));
teacher = getFreeColGameObject(in, "teacher", Unit.class);
student = getFreeColGameObject(in, "student", Unit.class);
final String indianSettlementStr = in.getAttributeValue(null, "indianSettlement");
if (indianSettlementStr != null) {
indianSettlement = (IndianSettlement) getGame().getFreeColGameObject(indianSettlementStr);
if (indianSettlement == null) {
indianSettlement = new IndianSettlement(getGame(), indianSettlementStr);
}
} else {
setIndianSettlement(null);
}
treasureAmount = getAttribute(in, "treasureAmount", 0);
destination = newLocation(in.getAttributeValue(null, "destination"));
currentStop = -1;
tradeRoute = null;
final String tradeRouteStr = in.getAttributeValue(null, "tradeRoute");
if (tradeRouteStr != null) {
tradeRoute = (TradeRoute) getGame().getFreeColGameObject(tradeRouteStr);
final String currentStopStr = in.getAttributeValue(null, "currentStop");
if (currentStopStr != null) {
currentStop = Integer.parseInt(currentStopStr);
}
}
workType = getSpecification().getType(in, "workType", GoodsType.class, null);
experienceType = getSpecification().getType(in, "experienceType", GoodsType.class, null);
if (experienceType == null && workType != null) {
experienceType = workType;
}
// @compat 0.9.x
try {
// this is likely to cause an exception, as the
// specification might not define grain
GoodsType grain = getSpecification().getGoodsType("model.goods.grain");
GoodsType food = getSpecification().getPrimaryFoodType();
if (food.equals(workType)) {
workType = grain;
}
if (food.equals(experienceType)) {
experienceType = grain;
}
} catch (Exception e) {
logger.log(Level.FINEST, "Failed to update food to grain.", e);
}
// end compatibility code
experience = getAttribute(in, "experience", 0);
visibleGoodsCount = getAttribute(in, "visibleGoodsCount", -1);
entryLocation = newLocation(in.getAttributeValue(null, "entryLocation"));
location = newLocation(in.getAttributeValue(null, "location"));
units.clear();
if (goodsContainer != null) goodsContainer.removeAll();
equipment.clear();
setWorkImprovement(null);
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (in.getLocalName().equals(UNITS_TAG_NAME)) {
units = new ArrayList<Unit>();
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (in.getLocalName().equals(Unit.getXMLElementTagName())) {
units.add(updateFreeColGameObject(in, Unit.class));
}
}
} else if (in.getLocalName().equals(GoodsContainer.getXMLElementTagName())) {
goodsContainer = (GoodsContainer) getGame().getFreeColGameObject(in.getAttributeValue(null, ID_ATTRIBUTE));
if (goodsContainer != null) {
goodsContainer.readFromXML(in);
} else {
goodsContainer = new GoodsContainer(getGame(), this, in);
}
} else if (in.getLocalName().equals(EQUIPMENT_TAG)) {
String xLength = in.getAttributeValue(null, ARRAY_SIZE);
if (xLength == null) {
String equipmentId = in.getAttributeValue(null, ID_ATTRIBUTE_TAG);
int count = Integer.parseInt(in.getAttributeValue(null, "count"));
equipment.incrementCount(getSpecification().getEquipmentType(equipmentId), count);
} else { // @compat 0.9.x
int length = Integer.parseInt(xLength);
for (int index = 0; index < length; index++) {
String equipmentId = in.getAttributeValue(null, "x" + String.valueOf(index));
equipment.incrementCount(getSpecification().getEquipmentType(equipmentId), 1);
}
} // end compatibility code
in.nextTag();
} else if (in.getLocalName().equals(TileImprovement.getXMLElementTagName())) {
setWorkImprovement(updateFreeColGameObject(in, TileImprovement.class));
} else {
logger.warning("Found unknown child element '" + in.getLocalName() + "' of Unit " + getId() + ", skipping to next tag.");
in.nextTag();
}
}
// ensure all carriers have a goods container, just in case
if (goodsContainer == null && getType().canCarryGoods()) {
logger.warning("Carrier with ID " + getId() + " did not have a \"goodsContainer\"-tag.");
goodsContainer = new GoodsContainer(getGame(), this);
}
setRole();
getOwner().setUnit(this);
getOwner().invalidateCanSeeTiles();
}
/**
* Partial writer for units, so that "remove" messages can be brief.
*
* @param out The target stream.
* @param fields The fields to write.
* @throws XMLStreamException If there are problems writing the stream.
*/
@Override
protected void toXMLPartialImpl(XMLStreamWriter out, String[] fields)
throws XMLStreamException {
toXMLPartialByClass(out, getClass(), fields);
}
/**
* Partial reader for units, so that "remove" messages can be brief.
*
* @param in The input stream with the XML.
* @throws XMLStreamException If there are problems reading the stream.
*/
@Override
protected void readFromXMLPartialImpl(XMLStreamReader in)
throws XMLStreamException {
readFromXMLPartialByClass(in, getClass());
}
/**
* Gets the tag name of the root element representing this object.
*
* @return "unit"
*/
public static String getXMLElementTagName() {
return "unit";
}
}