/** * 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"; } }