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