/** * 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.Iterator; import java.util.List; import java.util.Map.Entry; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; import net.sf.freecol.common.model.UnitTypeChange.ChangeType; public final class UnitType extends BuildableType implements Comparable<UnitType>, Consumer { /** * The default offence value. */ public static final int DEFAULT_OFFENCE = 0; /** * The default offence value. */ public static final int DEFAULT_DEFENCE = 1; /** * The offence of this UnitType. Only Units with an offence value * greater than zero can attack. */ private int offence = DEFAULT_OFFENCE; /** * The defence of this UnitType. */ private int defence = DEFAULT_DEFENCE; /** * The capacity of this UnitType. */ private int space = 0; /** * The number of hit points this UnitType has. At the moment, this * is only used for ships. All other UnitTypes are downgraded or * destroyed if they lose a battle. */ private int hitPoints = 0; /** * The space taken by this UnitType. */ private int spaceTaken = 1; /** * The skill level of this UnitType. */ private int skill = UNDEFINED; /** * The price of this UnitType. */ private int price = UNDEFINED; /** * The initial moves of this UnitType. */ private int movement = 3; /** * The maximum distance of tiles this UnitType can observe. */ private int lineOfSight = 1; /** * The probability of recruiting a Unit of this type in Europe. */ private int recruitProbability = 0; /** * The expert production of this UnitType. */ private GoodsType expertProduction; /** * How much a Unit of this type contributes to the Player's score. */ private int scoreValue = 0; /** * The maximum experience a unit of this type can accumulate. */ private int maximumExperience = 0; /** * The maximum attrition this UnitType can accumulate without * being destroyed. */ private int maximumAttrition = INFINITY; /** * The skill this UnitType teaches, mostly its own. */ private UnitType skillTaught; /** * Describe defaultEquipment here. */ private EquipmentType defaultEquipment; /** * The goods consumed per turn when in a settlement. */ private TypeCountMap<GoodsType> consumption = new TypeCountMap<GoodsType>(); /** * The possible type changes for this unit type. */ private List<UnitTypeChange> typeChanges = new ArrayList<UnitTypeChange>(); /** * Creates a new <code>UnitType</code> instance. * */ public UnitType(String id, Specification specification) { super(id, specification); setModifierIndex(Modifier.EXPERT_PRODUCTION_INDEX); } public final String getWorkingAsKey() { return getId() + ".workingAs"; } /** * Returns <code>true</code> if Units of this type can carry other Units. * * @return a <code>boolean</code> value */ public boolean canCarryUnits() { return hasAbility(Ability.CARRY_UNITS); } /** * Returns <code>true</code> if Units of this type can carry Goods. * * @return a <code>boolean</code> value */ public boolean canCarryGoods() { return hasAbility(Ability.CARRY_GOODS); } /** * Get the <code>ScoreValue</code> value. * * @return an <code>int</code> value */ public int getScoreValue() { return scoreValue; } /** * Set the <code>ScoreValue</code> value. * * @param newScoreValue The new ScoreValue value. */ public void setScoreValue(final int newScoreValue) { this.scoreValue = newScoreValue; } /** * Get the <code>Offence</code> value. * * @return an <code>int</code> value */ public int getOffence() { return offence; } /** * Set the <code>Offence</code> value. * * @param newOffence The new Offence value. */ public void setOffence(final int newOffence) { this.offence = newOffence; } /** * Get the <code>Defence</code> value. * * @return an <code>int</code> value */ public int getDefence() { return defence; } /** * Set the <code>Defence</code> value. * * @param newDefence The new Defence value. */ public void setDefence(final int newDefence) { this.defence = newDefence; } /** * Get the <code>LineOfSight</code> value. * * @return an <code>int</code> value */ public int getLineOfSight() { return lineOfSight; } /** * Set the <code>LineOfSight</code> value. * * @param newLineOfSight The new Defence value. */ public void setLineOfSight(final int newLineOfSight) { this.lineOfSight = newLineOfSight; } /** * Get the <code>Space</code> value. * * @return an <code>int</code> value */ public int getSpace() { return space; } /** * Set the <code>Space</code> value. * * @param newSpace The new Space value. */ public void setSpace(final int newSpace) { this.space = newSpace; } /** * Get the <code>HitPoints</code> value. * * @return an <code>int</code> value */ public int getHitPoints() { return hitPoints; } /** * Set the <code>HitPoints</code> value. * * @param newHitPoints The new HitPoints value. */ public void setHitPoints(final int newHitPoints) { this.hitPoints = newHitPoints; } /** * Get the <code>SpaceTaken</code> value. * * @return an <code>int</code> value */ public int getSpaceTaken() { return Math.max(spaceTaken, space + 1); } /** * Set the <code>SpaceTaken</code> value. * * @param newSpaceTaken The new SpaceTaken value. */ public void setSpaceTaken(final int newSpaceTaken) { this.spaceTaken = newSpaceTaken; } /** * If this UnitType is recruitable in Europe * * @return an <code>boolean</code> value */ public boolean isRecruitable() { return recruitProbability > 0; } /** * Get the <code>RecruitProbability</code> value. * * @return an <code>int</code> value */ public int getRecruitProbability() { return recruitProbability; } /** * Set the <code>RecruitProbability</code> value. * * @param newRecruitProbability The new RecruitProbability value. */ public void setRecruitProbability(final int newRecruitProbability) { this.recruitProbability = newRecruitProbability; } /** * Get the <code>Skill</code> value. * * @return an <code>int</code> value */ public int getSkill() { return skill; } /** * Set the <code>Skill</code> value. * * @param newSkill The new Skill value. */ public void setSkill(final int newSkill) { this.skill = newSkill; } /** * Get the <code>Price</code> value. * * @return an <code>int</code> value * * This returns the base price of the <code>UnitType</code> * * For the actual price of the unit, use {@link Europe#getUnitPrice(UnitType)} */ public int getPrice() { return price; } /** * Set the <code>Price</code> value. * * @param newPrice The new Price value. */ public void setPrice(final int newPrice) { this.price = newPrice; } /** * Get the <code>Movement</code> value. * * @return an <code>int</code> value */ public int getMovement() { return movement; } /** * Set the <code>Movement</code> value. * * @param newMovement The new Movement value. */ public void setMovement(final int newMovement) { this.movement = newMovement; } /** * Get the <code>MaximumExperience</code> value. * * @return an <code>int</code> value */ public final int getMaximumExperience() { return maximumExperience; } /** * Set the <code>MaximumExperience</code> value. * * @param newMaximumExperience The new MaximumExperience value. */ public final void setMaximumExperience(final int newMaximumExperience) { this.maximumExperience = newMaximumExperience; } /** * Get the <code>MaximumAttrition</code> value. * * @return an <code>int</code> value */ public int getMaximumAttrition() { return maximumAttrition; } /** * Set the <code>MaximumAttrition</code> value. * * @param newMaximumAttrition The new MaximumAttrition value. */ public void setMaximumAttrition(final int newMaximumAttrition) { this.maximumAttrition = newMaximumAttrition; } /** * Get the <code>ExpertProduction</code> value. * * @return a <code>GoodsType</code> value */ public GoodsType getExpertProduction() { return expertProduction; } /** * Set the <code>ExpertProduction</code> value. * * @param newExpertProduction The new ExpertProduction value. */ public void setExpertProduction(final GoodsType newExpertProduction) { this.expertProduction = newExpertProduction; } /** * Get the <code>DefaultEquipment</code> value. * * @return an <code>EquipmentType</code> value */ public EquipmentType getDefaultEquipmentType() { return defaultEquipment; } /** * Set the <code>DefaultEquipment</code> value. * * @param newDefaultEquipment The new DefaultEquipment value. */ public void setDefaultEquipmentType(final EquipmentType newDefaultEquipment) { this.defaultEquipment = newDefaultEquipment; } public EquipmentType[] getDefaultEquipment() { if (hasAbility(Ability.CAN_BE_EQUIPPED) && defaultEquipment != null) { int count = defaultEquipment.getMaximumCount(); EquipmentType[] result = new EquipmentType[count]; for (int index = 0; index < count; index++) { result[index] = defaultEquipment; } return result; } else { return EquipmentType.NO_EQUIPMENT; } } public List<UnitTypeChange> getTypeChanges() { return typeChanges; } /** * Get the <code>SkillTaught</code> value. * * @return an <code>UnitType</code> value */ public UnitType getSkillTaught() { return skillTaught; } /** * Set the <code>SkillTaught</code> value. * * @param newSkillTaught The new SkillTaught value. */ public void setSkillTaught(final UnitType newSkillTaught) { this.skillTaught = newSkillTaught; } /** * Returns true if the UnitType is available to the given * Player. * * @param player a <code>Player</code> value * @return a <code>boolean</code> value */ public boolean isAvailableTo(Player player) { java.util.Map<String, Boolean> requiredAbilities = getAbilitiesRequired(); for (Entry<String, Boolean> entry : requiredAbilities.entrySet()) { if (player.hasAbility(entry.getKey()) != entry.getValue()) { return false; } } return true; } /** * Describe <code>getUnitTypeChange</code> method here. * * @param changeType an <code>UnitTypeChange.Type</code> value * @param player a <code>Player</code> value * @return an <code>UnitType</code> value */ public UnitType getTargetType(ChangeType changeType, Player player) { UnitTypeChange change = getUnitTypeChange(changeType, player); return (change == null) ? null : change.getNewUnitType(); } /** * Describe <code>getUnitTypeChange</code> method here. * * @param changeType an <code>UnitTypeChange.Type</code> value * @param player a <code>Player</code> value * @return an <code>UnitType</code> value */ public UnitTypeChange getUnitTypeChange(ChangeType changeType, Player player) { for (UnitTypeChange change : typeChanges) { if (change.asResultOf(changeType) && change.appliesTo(player)) { UnitType result = change.getNewUnitType(); if (result.isAvailableTo(player)) { return change; } } } return null; } /** * Returns the <code>UnitTypeChange</code> associated with the * given <code>UnitType</code>, or <code>null</code> if there is * none. * * @param newType the target UnitType * @return the type change */ public UnitTypeChange getUnitTypeChange(UnitType newType) { for (UnitTypeChange change : typeChanges) { if (change.getNewUnitType() == newType) { return change; } } return null; } /** * Return true if this UnitType can be upgraded to the given * UnitType by the given means of education. If the given UnitType * is null, return true if the UnitType can be upgraded to any * other UnitType by the given means of education. * * @param newType The UnitType to learn (may be null in the case * of attempting to move to a native settlement when the skill * taught there is still unknown). * @param changeType an <code>ChangeType</code> value * @return <code>true</code> if can learn the given UnitType */ public boolean canBeUpgraded(UnitType newType, ChangeType changeType) { for (UnitTypeChange change : typeChanges) { if ((newType == null || newType == change.getNewUnitType()) && change.getProbability(changeType) > 0) { return true; } } return false; } /** * Get a list of UnitType which can learn in a lost city rumour * * @return <code>UnitType</code> with a skill equal or less than given * maximum */ public List<UnitType> getUnitTypesLearntInLostCity() { List<UnitType> unitTypes = new ArrayList<UnitType>(); for (UnitTypeChange change : typeChanges) { if (change.asResultOf(ChangeType.LOST_CITY)) { unitTypes.add(change.getNewUnitType()); } } return unitTypes; } /** * Get a UnitType to learn with a level skill less or equal than given level * * @param maximumSkill the maximum level skill which we are searching for * @return <code>UnitType</code> with a skill equal or less than given * maximum */ public UnitType getEducationUnit(int maximumSkill) { for (UnitTypeChange change : typeChanges) { if (change.canBeTaught()) { UnitType unitType = change.getNewUnitType(); if (unitType.hasSkill() && unitType.getSkill() <= maximumSkill) { return unitType; } } } return null; } /** * Get the <code>EducationTurns</code> value. * * @return a <code>int</code> value */ public int getEducationTurns(UnitType unitType) { for (UnitTypeChange change : typeChanges) { if (change.asResultOf(UnitTypeChange.ChangeType.EDUCATION)) { if (unitType == change.getNewUnitType()) { return change.getTurnsToLearn(); } } } return UNDEFINED; } /** * Is this a naval unit type? * * @return True if this is a naval unit type. */ public boolean isNaval() { return hasAbility(Ability.NAVAL_UNIT); } /** * Returns true if this UnitType has a skill. * * @return a <code>boolean</code> value */ public boolean hasSkill() { return skill != UNDEFINED; } /** * Returns true if this UnitType can be built. * * @return a <code>boolean</code> value */ public boolean canBeBuilt() { return getGoodsRequired().isEmpty() == false; } /** * Returns true if this UnitType has a price. * * @return a <code>boolean</code> value */ public boolean hasPrice() { return price != UNDEFINED; } /** * Returns the number of units of the given GoodsType this * UnitType consumes per turn (when in a settlement). * * @return units consumed */ public int getConsumptionOf(GoodsType goodsType) { return consumption.getCount(goodsType); } // Interface Comparable /** * {@inheritDoc} */ public int compareTo(UnitType other) { return getIndex() - other.getIndex(); } // Interface Consumer /** * Returns a list of GoodsTypes this Consumer consumes. * * @return a <code>List</code> value */ public List<AbstractGoods> getConsumedGoods() { List<AbstractGoods> result = new ArrayList<AbstractGoods>(); for (GoodsType goodsType : consumption.keySet()) { result.add(new AbstractGoods(goodsType, consumption.getCount(goodsType))); } 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() { // TODO: make this configurable return UNIT_PRIORITY; } /** * Makes an XML-representation of this object. * * @param out The output stream. * @throws XMLStreamException if there are any problems writing to the * stream. */ protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException { super.toXML(out, getXMLElementTagName()); } /** * Write the attributes of this object to a stream. * * @param out The target stream. * @throws XMLStreamException if there are any problems writing to * the stream. */ @Override protected void writeAttributes(XMLStreamWriter out) throws XMLStreamException { super.writeAttributes(out); out.writeAttribute("offence", Integer.toString(offence)); out.writeAttribute("defence", Integer.toString(defence)); out.writeAttribute("movement", Integer.toString(movement)); out.writeAttribute("lineOfSight", Integer.toString(lineOfSight)); out.writeAttribute("scoreValue", Integer.toString(scoreValue)); out.writeAttribute("space", Integer.toString(space)); out.writeAttribute("spaceTaken", Integer.toString(spaceTaken)); out.writeAttribute("hitPoints", Integer.toString(hitPoints)); out.writeAttribute("maximumExperience", Integer.toString(maximumExperience)); if (maximumAttrition < INFINITY) { out.writeAttribute("maximumAttrition", Integer.toString(maximumAttrition)); } out.writeAttribute("recruitProbability", Integer.toString(recruitProbability)); if (skill != UNDEFINED) { out.writeAttribute("skill", Integer.toString(skill)); } if (price != UNDEFINED) { out.writeAttribute("price", Integer.toString(price)); } out.writeAttribute("skillTaught", skillTaught.getId()); if (expertProduction != null) { out.writeAttribute("expert-production", expertProduction.getId()); } } /** * Write the children of this object to a stream. * * @param out The target stream. * @throws XMLStreamException if there are any problems writing to * the stream. */ @Override protected void writeChildren(XMLStreamWriter out) throws XMLStreamException { super.writeChildren(out); for (UnitTypeChange change : typeChanges) { change.toXMLImpl(out); } if (defaultEquipment != null) { out.writeStartElement("default-equipment"); out.writeAttribute(ID_ATTRIBUTE_TAG, defaultEquipment.getId()); out.writeEndElement(); } if (!consumption.isEmpty()) { for (GoodsType goodsType : consumption.keySet()) { out.writeStartElement("consumes"); out.writeAttribute(ID_ATTRIBUTE_TAG, goodsType.getId()); out.writeAttribute(VALUE_TAG, Integer.toString(consumption.getCount(goodsType))); out.writeEndElement(); } } } /** * Reads the attributes of this object from an XML stream. * * @param in The XML input stream. * @throws XMLStreamException if a problem was encountered * during parsing. */ @Override protected void readAttributes(XMLStreamReader in) throws XMLStreamException { super.readAttributes(in); String extendString = in.getAttributeValue(null, "extends"); UnitType parent = (extendString == null) ? this : getSpecification().getUnitType(extendString); offence = getAttribute(in, "offence", parent.offence); defence = getAttribute(in, "defence", parent.defence); movement = getAttribute(in, "movement", parent.movement); lineOfSight = getAttribute(in, "lineOfSight", parent.lineOfSight); scoreValue = getAttribute(in, "scoreValue", parent.scoreValue); space = getAttribute(in, "space", parent.space); hitPoints = getAttribute(in, "hitPoints", parent.hitPoints); spaceTaken = getAttribute(in, "spaceTaken", parent.spaceTaken); maximumExperience = getAttribute(in, "maximumExperience", parent.maximumExperience); maximumAttrition = getAttribute(in, "maximumAttrition", parent.maximumAttrition); String skillString = in.getAttributeValue(null, "skillTaught"); skillTaught = (skillString == null) ? this : getSpecification().getUnitType(skillString); recruitProbability = getAttribute(in, "recruitProbability", parent.recruitProbability); skill = getAttribute(in, "skill", parent.skill); setPopulationRequired(getAttribute(in, "population-required", parent.getPopulationRequired())); price = getAttribute(in, "price", parent.price); expertProduction = getSpecification().getType(in, "expert-production", GoodsType.class, parent.expertProduction); if (parent != this) { typeChanges.addAll(parent.typeChanges); defaultEquipment = parent.defaultEquipment; consumption.putAll(parent.consumption); getFeatureContainer().add(parent.getFeatureContainer()); if (parent.isAbstractType()) { getFeatureContainer().replaceSource(parent, this); } } } /** * Reads a child object. * * @param in The XML stream to read. * @exception XMLStreamException if an error occurs */ @Override protected void readChild(XMLStreamReader in) throws XMLStreamException { String nodeName = in.getLocalName(); Specification spec = getSpecification(); if ("downgrade".equals(nodeName) || "upgrade".equals(nodeName)) { if (getAttribute(in, "delete", false)) { String unitId = in.getAttributeValue(null, "unit"); Iterator<UnitTypeChange> iterator = typeChanges.iterator(); while (iterator.hasNext()) { if (unitId.equals(iterator.next() .getNewUnitType().getId())) { iterator.remove(); break; } } in.nextTag(); } else { UnitTypeChange change = new UnitTypeChange(in, spec); if ("downgrade".equals(nodeName) && change.getChangeTypes().isEmpty()) { // add default downgrade type change.getChangeTypes().put(ChangeType.CLEAR_SKILL, 100); } typeChanges.add(change); } } else if ("default-equipment".equals(nodeName)) { String equipmentString = in.getAttributeValue(null, ID_ATTRIBUTE_TAG); if (equipmentString != null) { defaultEquipment = spec.getEquipmentType(equipmentString); } in.nextTag(); // close this element } else if ("consumes".equals(nodeName)) { String typeString = in.getAttributeValue(null, ID_ATTRIBUTE_TAG); String valueString = in.getAttributeValue(null, VALUE_TAG); if (typeString != null && valueString != null) { try { GoodsType type = spec.getGoodsType(typeString); int amount = Integer.parseInt(valueString); consumption.incrementCount(type, amount); } catch(Exception e) { logger.warning("Failed to parse integer " + valueString); } } in.nextTag(); // close this element } else { super.readChild(in); } } /** * Debug print helper. */ public String toString() { return getId(); } /** * Returns the tag name of the root element representing this object. * * @return "unit-type". */ public static String getXMLElementTagName() { return "unit-type"; } }