/**
* 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.Map.Entry;
import java.util.Set;
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.Tension.Level;
/**
* Represents an Indian settlement.
*/
public class IndianSettlement extends Settlement {
private static final Logger logger = Logger.getLogger(IndianSettlement.class.getName());
public static final int TALES_RADIUS = 6;
public static final String OWNED_UNITS_TAG_NAME = "ownedUnits";
public static final String IS_VISITED_TAG_NAME = "isVisited";
public static final String ALARM_TAG_NAME = "alarm";
public static final String MISSIONARY_TAG_NAME = "missionary";
public static final String WANTED_GOODS_TAG_NAME = "wantedGoods";
// Do not sell less than this amount of goods.
public static final int TRADE_MINIMUM_SIZE = 20;
// Do not buy goods when the price is this low.
public static final int TRADE_MINIMUM_PRICE = 3;
public static final int GOODS_BASE_PRICE = 12;
/** The amount of goods a brave can produce a single turn. */
//private static final int WORK_AMOUNT = 5;
/**
* The amount of raw material that should be available before
* producing manufactured goods.
*/
public static final int KEEP_RAW_MATERIAL = 50;
/**
* This is the skill that can be learned by Europeans at this
* settlement. At the server side its value will be null when the
* skill has already been taught to a European. At the client
* side the value null is also possible in case the player hasn't
* checked out the settlement yet.
*/
protected UnitType learnableSkill = null;
/** The goods this settlement wants. */
protected GoodsType[] wantedGoods = new GoodsType[] {null, null, null};
/**
* A map that tells if a player has spoken to the chief of this settlement.
*
* At the client side, only the information regarding the player
* on that client should be included.
*/
protected Set<Player> spokenTo = new HashSet<Player>();
/** Units that belong to this settlement. */
protected ArrayList<Unit> ownedUnits = new ArrayList<Unit>();
/** The missionary at this settlement. */
protected Unit missionary = null;
/** Used for monitoring the progress towards creating a convert. */
protected int convertProgress = 0;
/** The number of the turn during which the last tribute was paid. */
protected int lastTribute = 0;
/**
* Stores the alarm levels. <b>Only used by AI.</b>
* "Alarm" means: Tension with respect to a player from an
* IndianSettlement.
* Alarm is overloaded with the concept of "contact". If a settlement
* has never been contacted by a player, alarm.get(player) will be null.
* Acts causing contact initialize this variable.
*/
private java.util.Map<Player, Tension> alarm
= new HashMap<Player, Tension>();
// When choosing what goods to buy, sort goods types descending by price.
private final Comparator<GoodsType> wantedGoodsComparator
= new Comparator<GoodsType>() {
public int compare(GoodsType goodsType1, GoodsType goodsType2) {
return (getNormalGoodsPriceToBuy(goodsType2,
GoodsContainer.CARGO_SIZE)
- getNormalGoodsPriceToBuy(goodsType1,
GoodsContainer.CARGO_SIZE));
}
};
// When choosing what goods to sell, sort goods with new world
// goods first, then by price, then amount.
private final Comparator<Goods> exportGoodsComparator
= new Comparator<Goods>() {
public int compare(Goods goods1, Goods goods2) {
int cmp;
GoodsType t1 = goods1.getType();
GoodsType t2 = goods2.getType();
cmp = (((t2.isNewWorldGoodsType()) ? 1 : 0)
- ((t1.isNewWorldGoodsType()) ? 1 : 0));
if (cmp == 0) {
int a1 = Math.min(goods2.getAmount(),
GoodsContainer.CARGO_SIZE);
int a2 = Math.min(goods1.getAmount(),
GoodsContainer.CARGO_SIZE);
cmp = getPriceToSell(t2, a2) - getPriceToSell(t1, a1);
if (cmp == 0) {
cmp = a2 - a1;
}
}
return cmp;
}
};
/**
* Constructor for ServerIndianSettlement.
*/
protected IndianSettlement() {
// empty constructor
}
/**
* Constructor for ServerIndianSettlement.
*
* @param game The <code>Game</code> in which this object belong.
* @param owner The <code>Player</code> owning this settlement.
* @param name The name for this settlement.
* @param tile The location of the <code>IndianSettlement</code>.
*/
protected IndianSettlement(Game game, Player owner, String name,
Tile tile) {
super(game, owner, name, tile);
}
/**
* Initiates a new <code>IndianSettlement</code> from an <code>Element</code>.
*
* @param game The <code>Game</code> in which this object belong.
* @param in The input stream containing the XML.
* @throws XMLStreamException if a problem was encountered
* during parsing.
*/
public IndianSettlement(Game game, XMLStreamReader in) throws XMLStreamException {
super(game, in);
readFromXML(in);
}
/**
* Initiates a new <code>IndianSettlement</code>
* with the given ID. The object should later be
* initialized by calling either
* {@link #readFromXML(XMLStreamReader)}.
*
* @param game The <code>Game</code> in which this object belong.
* @param id The unique identifier for this object.
*/
public IndianSettlement(Game game, String id) {
super(game, id);
}
/**
* Gets the name of this <code>Settlement</code> for a particular player.
*
* @param player A <code>Player</code> to return the name for.
* @return The name as a <code>String</code>.
*/
public String getNameFor(Player player) {
return (hasContactedSettlement(player)) ? getName()
: "indianSettlement.nameUnknown";
}
/**
* Gets the image key for this native settlement.
*
* @return The image key.
*/
public String getImageKey() {
return getOwner().getNationID()
+ (isCapital() ? ".capital" : ".settlement")
+ ((getMissionary() == null) ? "" : ".mission")
+ ".image";
}
/**
* Returns a suitable (non-unique) name.
* @return The name of this settlement.
*/
public StringTemplate getLocationName() {
return StringTemplate.name(getName());
}
/**
* Returns a suitable (non-unique) name for a particular player.
*
* @param player The <code>Player</code> to prepare the name for.
* @return The name of this settlement.
*/
public StringTemplate getLocationNameFor(Player player) {
return StringTemplate.name(getNameFor(player));
}
/**
* Get the year of the last tribute.
*
* @return The year of the last tribute.
*/
public int getLastTribute() {
return lastTribute;
}
/**
* Set the year of the last tribute.
*
* @param lastTribute The new last tribute year.
*/
public void setLastTribute(int lastTribute) {
this.lastTribute = lastTribute;
}
/**
* Gets the alarm level towards the given player.
*
* @param player The <code>Player</code> to get the alarm level for.
* @return The current alarm level or null if the settlement has not
* encoutered the player.
*/
public Tension getAlarm(Player player) {
return alarm.get(player);
}
/**
* Sets alarm towards the given player.
*
* @param newAlarm The new alarm value.
*/
public void setAlarm(Player player, Tension newAlarm) {
if (player != null && player != owner) alarm.put(player, newAlarm);
}
/**
* Removes all alarm towards the given player. Used the a player leaves
* the game.
*
* @param player The <code>Player</code> to remove the alarm for.
*/
public void removeAlarm(Player player) {
if (player != null) alarm.remove(player);
}
/**
* Change the alarm level of this settlement by a given amount.
*
* @param player The <code>Player</code> the alarm level changes wrt.
* @param amount The amount to change the alarm by.
* @return True if the <code>Tension.Level</code> of the
* settlement alarm changes as a result of this change.
*/
protected boolean changeAlarm(Player player, int amount) {
Tension alarm = getAlarm(player);
Level oldLevel = alarm.getLevel();
alarm.modify(amount);
return oldLevel != alarm.getLevel();
}
/**
* Gets a messageId for a short alarm message associated with the
* alarm level of this player.
*
* @param player The other <code>Player</code>.
* @return The alarm messageId.
*/
public String getShortAlarmLevelMessageId(Player player) {
return (!player.hasContacted(owner)) ? "tension.wary"
: (hasContactedSettlement(player)) ? getAlarm(player).getKey()
: "indianSettlement.tensionUnknown";
}
/**
* Gets a messageId for an alarm message associated with the
* alarm level of this player.
*
* @param player The other <code>Player</code>.
* @return The alarm messageId.
*/
public String getAlarmLevelMessageId(Player player) {
Tension alarm = (hasContactedSettlement(player)) ? getAlarm(player)
: new Tension(Tension.TENSION_MIN);
return "indianSettlement.alarm." + alarm.getKey();
}
/**
* Has a player contacted this settlement?
*
* @param player The <code>Player</code> to check.
* @return True if the player has contacted this settlement.
*/
public boolean hasContactedSettlement(Player player) {
return getAlarm(player) != null;
}
/**
* Make contact with this settlement (if it has not been
* previously contacted). Initialize tension level to the general
* level with respect to the contacting player--- effectively the
* average reputation of this player with the overall tribe.
*
* @param player The <code>Player</code> making contact.
* @return True if this was indeed the first contact between settlement
* and player.
*/
public boolean makeContactSettlement(Player player) {
if (!hasContactedSettlement(player)) {
setAlarm(player, new Tension(owner.getTension(player).getValue()));
return true;
}
return false;
}
/**
* Propagates a global change in tension down to a settlement.
* Only apply the change if the settlement is aware of the player
* causing alarm.
*
* @param player The <code>Player</code> towards whom the alarm is felt.
* @param addToAlarm The amount to add to the current alarm level.
* @return True if the alarm level changes as a result of this change.
*/
public boolean propagateAlarm(Player player, int addToAlarm) {
if (hasContactedSettlement(player)) {
return changeAlarm(player, addToAlarm);
}
return false;
}
/**
* Returns true if a European player has spoken with the chief of
* this settlement.
*
* @return True if a European player has spoken with the chief.
*/
public boolean hasSpokenToChief() {
Iterator<Player> playerIterator = spokenTo.iterator();
while (playerIterator.hasNext()) {
if (playerIterator.next().isEuropean()) {
return true;
}
}
return false;
}
/**
* Returns true if a the given player has spoken with the chief of
* this settlement.
*
* @param player The <code>Player</code> to check.
* @return True if the player has visited this settlement to speak
* with the chief.
*/
public boolean hasSpokenToChief(Player player) {
return spokenTo.contains(player);
}
/**
* Sets the spoken-to status of this settlement to true, indicating
* that a European player has had a chat with the chief.
*
* @param player The visiting <code>Player</code>.
*/
public void setSpokenToChief(Player player) {
if (!hasSpokenToChief(player)) {
makeContactSettlement(player); // Just to be sure
spokenTo.add(player);
}
}
/**
* Is a unit permitted to make contact with this settlement?
* The unit must be from a nation that has already made contact,
* or in the first instance, must be arriving by land, with the
* exception of trading ships.
*
* @param unit The <code>Unit</code> that proposes to contact this
* settlement.
* @return True if the settlement accepts such contact.
*/
public boolean allowContact(Unit unit) {
return unit.getOwner().hasContacted(owner)
|| !unit.isNaval()
|| unit.getGoodsCount() > 0;
}
/**
* Adds the given <code>Unit</code> to the list of units that belongs to this
* <code>IndianSettlement</code>.
*
* @param unit The <code>Unit</code> to be added.
*/
public void addOwnedUnit(Unit unit) {
if (unit == null) {
throw new IllegalArgumentException("Parameter 'unit' must not be 'null'.");
}
if (!ownedUnits.contains(unit)) {
ownedUnits.add(unit);
}
}
/**
* Gets a list of the units native to this settlement.
*
* @return The list of units native to this settlement.
*/
public List<Unit> getOwnedUnits() {
return new ArrayList<Unit>(ownedUnits);
}
/**
* Gets an iterator over all the units this
* <code>IndianSettlement</code> is owning.
*
* @return The <code>Iterator</code>.
*/
public Iterator<Unit> getOwnedUnitsIterator() {
return ownedUnits.iterator();
}
/**
* Removes the given <code>Unit</code> to the list of units that
* belongs to this <code>IndianSettlement</code>. Returns true if
* the Unit was removed.
*
* @param unit The <code>Unit</code> to be removed from the
* list of the units this <code>IndianSettlement</code>
* owns.
* @return a <code>boolean</code> value
*/
public boolean removeOwnedUnit(Unit unit) {
if (unit == null) {
throw new IllegalArgumentException("Parameter 'unit' must not be 'null'.");
}
return ownedUnits.remove(unit);
}
/**
* Returns the skill that can be learned at this settlement.
* @return The skill that can be learned at this settlement.
*/
public UnitType getLearnableSkill() {
return learnableSkill;
}
/**
* Gets the missionary from this settlement.
*
* @return The missionary at this settlement, or null if there is none.
*/
public Unit getMissionary() {
return missionary;
}
/**
* Sets the missionary for this settlement.
*
* @param missionary The missionary for this settlement.
*/
public void setMissionary(Unit missionary) {
this.missionary = missionary;
}
/**
* Changes the missionary for this settlement and updates other players.
*
* @param missionary The new missionary for this settlement.
*/
public void changeMissionary(Unit missionary) {
setMissionary(missionary);
getTile().updatePlayerExploredTiles();
if (missionary != null) {
// Full update for the new missionary owner
getTile().updatePlayerExploredTile(missionary.getOwner(), true);
}
}
/**
* Gets the missionary from this settlement if there is one and
* it is owned by a specified player.
*
* @param player The player purported to own the missionary
* @return The missionary from this settlement if there is one and
* it is owned by the specified player, otherwise null.
*/
public Unit getMissionary(Player player) {
return (missionary == null || missionary.getOwner() != player) ? null
: missionary;
}
/**
* Gets the convert progress status for this settlement.
*
* @return The convert progress status.
*/
public int getConvertProgress() {
return convertProgress;
}
/**
* Sets the convert progress status for this settlement.
*
* @param progress The new convert progress status.
*/
public void setConvertProgress(int progress) {
convertProgress = progress;
}
public GoodsType[] getWantedGoods() {
return wantedGoods;
}
public void setWantedGoods(int index, GoodsType type) {
if (0 <= index && index < wantedGoods.length) {
wantedGoods[index] = type;
}
}
/**
* Sets the learnable skill for this Indian settlement.
* @param skill The new learnable skill for this Indian settlement.
*/
public void setLearnableSkill(UnitType skill) {
learnableSkill = skill;
}
/**
* Adds a <code>Locatable</code> to this Location.
*
* @param locatable The <code>Locatable</code> to add to this Location.
*/
public boolean add(Locatable locatable) {
boolean result = super.add(locatable);
if (result && locatable instanceof Unit) {
Unit indian = (Unit)locatable;
if (indian.getIndianSettlement() == null) {
// Adopt homeless Indians
indian.setIndianSettlement(this);
}
}
return result;
}
/**
* Gets the <code>Unit</code> that is currently defending this
* <code>IndianSettlement</code>.
*
* @param attacker The unit that would be attacking this
* <code>IndianSettlement</code>.
* @return The <code>Unit</code> that has been chosen to defend
* this <code>IndianSettlement</code>.
*/
@Override
public Unit getDefendingUnit(Unit attacker) {
Unit defender = null;
float defencePower = -1.0f;
for (Unit nextUnit : getUnitList()) {
float unitPower = attacker.getGame().getCombatModel()
.getDefencePower(attacker, nextUnit);
if (Unit.betterDefender(defender, defencePower,
nextUnit, unitPower)) {
defender = nextUnit;
defencePower = unitPower;
}
}
return defender;
}
/**
* Gets the range of gold plunderable when this settlement is captured.
*/
public RandomRange getPlunderRange(Unit attacker) {
return getType().getPlunderRange(attacker);
}
/**
* Gets the storage capacity of this settlement.
*
* @return The storage capacity of this settlement.
*/
public int getGoodsCapacity() {
return getType().getWarehouseCapacity();
}
/**
* Gets the amount of gold this <code>IndianSettlment</code>
* is willing to pay for the given <code>Goods</code>.
*
* It is only meaningful to call this method from the
* server, since the settlement's {@link GoodsContainer}
* is hidden from the clients.
*
* @param goods The <code>Goods</code> to price.
* @return The price.
*/
public int getPriceToBuy(Goods goods) {
return getPriceToBuy(goods.getType(), goods.getAmount());
}
/**
* Gets the amount of gold this <code>IndianSettlment</code>
* is willing to pay for the given <code>Goods</code>.
*
* It is only meaningful to call this method from the server,
* since the settlement's {@link GoodsContainer} is hidden from
* the clients. The AI uses it though so it stays here for now.
* Note that it takes no account of whether the native player
* actually has the gold.
*
* TODO: this is rancid with magic numbers.
* TODO: the hardwired goods/equipment types are a wart.
*
* @param type The type of <code>Goods</code> to price.
* @param amount The amount of <code>Goods</code> to price.
* @return The price.
*/
public int getPriceToBuy(GoodsType type, int amount) {
if (amount > GoodsContainer.CARGO_SIZE) {
throw new IllegalArgumentException("Amount > "
+ GoodsContainer.CARGO_SIZE);
}
int price = 0;
if (type.isMilitaryGoods()) {
// Might return zero if a surplus is present
price = getMilitaryGoodsPriceToBuy(type, amount);
}
if (price == 0) {
price = getNormalGoodsPriceToBuy(type, amount);
}
// Apply wanted bonus
final int wantedBase = 100; // Granularity for wanted bonus
final int wantedBonus // Premium paid for wanted goods types
= (type == wantedGoods[0]) ? 150
: (type == wantedGoods[1]) ? 125
: (type == wantedGoods[2]) ? 110
: 100;
// Do not simplify with *=, we want the integer truncation.
price = wantedBonus * price / wantedBase;
logger.finest("Full price(" + amount + " " + type + ")"
+ " -> " + price);
return price;
}
/**
* Price some goods according to the amount present in the settlement.
*
* @param type The type of goods for sale.
* @param amount The amount of goods for sale.
* @return A price for the goods.
*/
private int getNormalGoodsPriceToBuy(GoodsType type, int amount) {
final int tradeGoodsAdd = 20; // Fake additional trade goods present
final int capacity = getGoodsCapacity();
int current = getGoodsCount(type);
// Increase effective stock if its raw material is produced here.
GoodsType rawType = type.getRawMaterial();
if (rawType != null) {
int rawProduction = getMaximumProduction(rawType);
int add = (rawProduction < 5) ? 10 * rawProduction
: (rawProduction < 10) ? 5 * rawProduction + 25
: (rawProduction < 20) ? 2 * rawProduction + 55
: 100;
// Decrease bonus in proportion to current stock, up to capacity.
add = add * Math.max(0, capacity - current) / capacity;
current += add;
} else if (type.isTradeGoods()) {
// Small artificial increase of the trade goods stored.
current += tradeGoodsAdd;
}
// Only interested in the amount of goods that keeps the
// total under the threshold.
int retain = Math.min(getWantedGoodsAmount(type), capacity);
int valued = (retain <= current) ? 0
: Math.min(amount, retain - current);
// Unit price then is maximum price plus the bonus for the
// settlement type, reduced by the proportion of goods present.
int unitPrice = (GOODS_BASE_PRICE + getType().getTradeBonus())
* Math.max(0, capacity - current) / capacity;
// But farmed goods are always less interesting.
// and small settlements are not interested in building.
if (type.isFarmed() || type.isRawBuildingMaterial()) unitPrice /= 2;
// Only pay for the portion that is valued.
int price = (unitPrice < 0) ? 0 : valued * unitPrice;
logger.finest("Normal price(" + amount + " " + type + ")"
+ " valued=" + valued
+ " current=" + getGoodsCount(type)
+ " + " + (current - getGoodsCount(type))
+ " unitPrice=" + unitPrice
+ " -> " + price);
return price;
}
/**
* Calculates how much of the given goods type this settlement
* wants and should retain.
*
* @param type The <code>GoodsType</code>.
* @return The amount of goods wanted.
*/
protected int getWantedGoodsAmount(GoodsType type) {
final Specification spec = getSpecification();
if (type.isMilitaryGoods()) {
// Retain enough goods to fully arm.
int need = 0;
int toArm = 0;
if (type == spec.getGoodsType("model.goods.muskets")) {
for (Unit u : ownedUnits) if (!u.isArmed()) need++;
toArm = spec.getEquipmentType("model.equipment.indian.muskets")
.getAmountRequiredOf(type);
} else if (type == spec.getGoodsType("model.goods.horses")) {
for (Unit u : ownedUnits) if (!u.isMounted()) need++;
toArm = spec.getEquipmentType("model.equipment.indian.horses")
.getAmountRequiredOf(type);
}
return need * toArm;
}
int consumption = getConsumptionOf(type);
if (type == spec.getPrimaryFoodType()) {
// Food is perishable, do not try to retain that much
return Math.max(40, consumption * 3);
}
if (type.isTradeGoods() || type.isNewWorldLuxuryType()
|| type.isRefined()) {
// Aim for 10 years supply, resupply is doubtful
return Math.max(80, consumption * 20);
}
// Just keep some around
return 2 * getUnitCount();
}
/**
* Price some goods that have military value to the settlement.
*
* @param type The type of goods for sale.
* @param amount The amount of goods for sale.
* @return A price for the goods.
*/
private int getMilitaryGoodsPriceToBuy(GoodsType type, int amount) {
final int full = GOODS_BASE_PRICE + getType().getTradeBonus();
int required = getWantedGoodsAmount(type);
if (required == 0) return 0; // Do not pay military price
// If the settlement can use more than half of the goods on offer,
// then pay top dollar for the lot. Otherwise only pay the premium
// price for the part they need and refer the remaining amount to
// the normal goods pricing.
int valued = Math.max(0, required - getGoodsCount(type));
int price = (valued > amount / 2) ? full * amount
: valued * full + getNormalGoodsPriceToBuy(type, amount - valued);
logger.finest("Military price(" + amount + " " + type + ")"
+ " valued=" + valued
+ " -> " + price);
return price;
}
/**
* Gets the amount of gold this <code>IndianSettlment</code>
* is willing to sell the given <code>Goods</code> for.
*
* It is only meaningful to call this method from the
* server, since the settlement's {@link GoodsContainer}
* is hidden from the clients.
*
* @param goods The <code>Goods</code> to price.
* @return The price.
*/
public int getPriceToSell(Goods goods) {
return getPriceToSell(goods.getType(), goods.getAmount());
}
/**
* Gets the amount of gold this <code>IndianSettlment</code>
* is willing to sell the given <code>Goods</code> for.
*
* It is only meaningful to call this method from the
* server, since the settlement's {@link GoodsContainer}
* is hidden from the clients.
*
* @param type The type of <code>Goods</code> to price.
* @param amount The amount of <code>Goods</code> to price.
* @return The price.
*/
public int getPriceToSell(GoodsType type, int amount) {
if (amount > GoodsContainer.CARGO_SIZE) {
throw new IllegalArgumentException("Amount > "
+ GoodsContainer.CARGO_SIZE);
}
final int full = GOODS_BASE_PRICE + getType().getTradeBonus();
// Base price is purchase price plus delta.
// - military goods at double value
// - trade goods at +50%
int price = amount + Math.max(0, 11 * getPriceToBuy(type, amount) / 10);
if (type.isMilitaryGoods()) {
price = Math.max(price, amount * full * 2);
} else if (type.isTradeGoods()) {
price = Math.max(price, 150 * amount * full / 100);
}
return price;
}
/**
* Will this settlement sell a type of goods.
* Placeholder until we have a spec-configured blacklist.
*
* @param type The <code>GoodsType</code> to consider.
* @return True if the settlement would sell the goods.
*/
public boolean willSell(GoodsType type) {
return !type.isTradeGoods();
}
/**
* Gets the goods this settlement is willing to sell.
*
* @param limit The maximum number of goods required.
* @param unit The <code>Unit</code> that is trading.
* @return A list of goods to sell.
*/
public List<Goods> getSellGoods(int limit, Unit unit) {
List<Goods> result = new ArrayList<Goods>();
List<Goods> settlementGoods = getCompactGoods();
Collections.sort(settlementGoods, exportGoodsComparator);
int count = 0;
for (Goods goods : settlementGoods) {
if (!willSell(goods.getType())) continue;
int amount = goods.getAmount();
int retain = getWantedGoodsAmount(goods.getType());
if (retain >= amount) continue;
amount -= retain;
if (amount > GoodsContainer.CARGO_SIZE) {
amount = GoodsContainer.CARGO_SIZE;
}
if (unit != null) {
amount = Math.round(FeatureContainer
.applyModifierSet((float) amount, getGame().getTurn(),
unit.getModifierSet("model.modifier.tradeVolumePenalty")));
}
if (amount < TRADE_MINIMUM_SIZE) continue;
result.add(new Goods(getGame(), this, goods.getType(), amount));
count++;
if (count >= limit) break;
}
return result;
}
/**
* Allows spread of horses and arms between settlements
* @param settlement
*/
public void tradeGoodsWithSetlement(IndianSettlement settlement) {
GoodsType armsType = getSpecification().getGoodsType("model.goods.muskets");
GoodsType horsesType = getSpecification().getGoodsType("model.goods.horses");
List<GoodsType> goodsToTrade = new ArrayList<GoodsType>();
goodsToTrade.add(armsType);
goodsToTrade.add(horsesType);
for(GoodsType goods : goodsToTrade){
int goodsInStock = getGoodsCount(goods);
if(goodsInStock <= 50){
continue;
}
int goodsTraded = goodsInStock / 2;
settlement.addGoods(goods, goodsTraded);
removeGoods(goods, goodsTraded);
}
}
/**
* Gets the maximum possible production of the given type of goods.
* @param goodsType The type of goods to check.
* @return The maximum amount, of the given type of goods, that can
* be produced in one turn.
*/
public int getMaximumProduction(GoodsType goodsType) {
int amount = 0;
for (Tile workTile: getTile().getSurroundingTiles(getRadius())) {
if (workTile.getOwningSettlement() == null || workTile.getOwningSettlement() == this) {
// TODO: make unitType brave
amount += workTile.potential(goodsType, null);
}
}
return amount;
}
/**
* Updates the goods wanted by this settlement.
*
* It is only meaningful to call this method from the
* server, since the settlement's {@link GoodsContainer}
* is hidden from the clients.
*/
public void updateWantedGoods() {
List<GoodsType> goodsTypes = new ArrayList<GoodsType>(getSpecification().getGoodsTypeList());
Collections.sort(goodsTypes, wantedGoodsComparator);
int wantedIndex = 0;
for (GoodsType goodsType : goodsTypes) {
// The natives do not trade military or non-storable goods.
if (goodsType.isMilitaryGoods() || !goodsType.isStorable())
continue;
if (getNormalGoodsPriceToBuy(goodsType, GoodsContainer.CARGO_SIZE)
<= GoodsContainer.CARGO_SIZE * TRADE_MINIMUM_PRICE
|| wantedIndex >= wantedGoods.length) break;
wantedGoods[wantedIndex] = goodsType;
wantedIndex++;
}
for (; wantedIndex < wantedGoods.length; wantedIndex++) {
wantedGoods[wantedIndex] = null;
}
}
/**
* Chooses a type of goods for some of the natives in a settlement
* to manufacture.
* Simple rule: choose the refined goods that is the greatest shortage
* for which there is a surplus of the raw material.
*
* @return A <code>GoodsType</code> to manufacture, or null if
* none suitable.
*/
private GoodsType goodsToMake() {
GoodsType wantGoods = null;
int diff, wantAmount = -1;
for (GoodsType g : getSpecification().getGoodsTypeList()) {
GoodsType produced;
if (g.isRawMaterial()
&& (produced = g.getProducedMaterial()) != null
&& produced.isStorable()
&& getGoodsCount(g) > getWantedGoodsAmount(g)
&& (diff = getWantedGoodsAmount(produced)
- getGoodsCount(produced)) > wantAmount) {
wantGoods = produced;
wantAmount = diff;
}
}
return wantGoods;
}
/**
* Gets the production of a specified goods type for this settlement.
*
* @param type The <code>GoodsType</code> to produce.
* @return The production potention for the goods type.
*/
public int getProductionOf(GoodsType type) {
if (type.isRefined()) {
if (type != goodsToMake()) return 0;
// Pretend 1/3 of the units present make the item with
// basic production of 3.
return getUnitCount();
}
int potential = 0;
int tiles = 1; // Always include center tile
for (Tile workTile : getOwnedTiles()) {
if (workTile != getTile() && !workTile.isOccupied()) {
// TODO: make unitType brave
potential += workTile.potential(type, null);
tiles++;
}
}
// When a native settlement has more tiles than units, pretend
// that they produce from their entire area at reduced
// efficiency.
if (tiles > getUnitCount()) {
potential *= (float) getUnitCount() / tiles;
}
// But always add full potential of the center tile.
potential += getTile().potential(type, null);
return potential;
}
/**
* Native settlements do not generate SoL.
*
* @return 0.
*/
public int getSoL() {
return 0;
}
public boolean checkForNewMissionaryConvert() {
/* Increase convert progress and generate convert if needed. */
if (missionary != null && getGame().getViewOwner() == null) {
int increment = 8;
// Update increment if missionary is an expert.
if (missionary.hasAbility("model.ability.expertMissionary")) {
increment = 13;
}
// Increase increment if alarm level is high.
increment += 2 * getAlarm(missionary.getOwner()).getValue() / 100;
convertProgress += increment;
if (convertProgress >= 100 && getUnitCount() > 2) {
convertProgress = 0;
return true;
}
}
return false;
}
/**
* Dispose of this native settlement.
*
* @return A list of disposed objects.
*/
@Override
public List<FreeColGameObject> disposeList() {
// Orphan the units whose home settlement this is.
while (ownedUnits.size() > 0) {
ownedUnits.remove(0).setIndianSettlement(null);
}
return super.disposeList();
}
/**
* 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.
*/
@Override
protected void toXMLImpl(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
boolean full = showAll || toSavedGame || player == getOwner();
PlayerExploredTile pet = (player == null) ? null
: getTile().getPlayerExploredTile(player);
if (toSavedGame && !showAll) {
logger.warning("toSavedGame is true, but showAll is false");
}
// Start element:
out.writeStartElement(getXMLElementTagName());
super.writeAttributes(out);
if (full) {
out.writeAttribute("lastTribute", Integer.toString(lastTribute));
out.writeAttribute("convertProgress", Integer.toString(convertProgress));
writeAttribute(out, "learnableSkill", learnableSkill);
for (int i = 0; i < wantedGoods.length; i++) {
if (wantedGoods[i] != null) {
String tag = "wantedGoods" + Integer.toString(i);
out.writeAttribute(tag, wantedGoods[i].getId());
}
}
} else if (pet != null) {
writeAttribute(out, "learnableSkill", pet.getSkill());
GoodsType[] wanted = pet.getWantedGoods();
int i, j = 0;
for (i = 0; i < wanted.length; i++) {
if (wanted[i] != null) {
String tag = "wantedGoods" + Integer.toString(j);
out.writeAttribute(tag, wanted[i].getId());
j++;
}
}
}
writeChildren(out, player, showAll, toSavedGame);
out.writeEndElement();
}
/**
* {@inheritDoc}
*/
protected void writeChildren(XMLStreamWriter out, Player player,
boolean showAll, boolean toSavedGame)
throws XMLStreamException {
PlayerExploredTile pet;
if (showAll || toSavedGame || player == getOwner()) {
for (Player p : spokenTo) {
out.writeStartElement(IS_VISITED_TAG_NAME);
out.writeAttribute("player", p.getId());
out.writeEndElement();
}
for (Entry<Player, Tension> entry : alarm.entrySet()) {
out.writeStartElement(ALARM_TAG_NAME);
out.writeAttribute("player", entry.getKey().getId());
out.writeAttribute(VALUE_TAG,
String.valueOf(entry.getValue().getValue()));
out.writeEndElement();
}
if (missionary != null) {
out.writeStartElement(MISSIONARY_TAG_NAME);
missionary.toXML(out, player, showAll, toSavedGame);
out.writeEndElement();
}
for (Unit unit : ownedUnits) {
out.writeStartElement(OWNED_UNITS_TAG_NAME);
out.writeAttribute(ID_ATTRIBUTE, unit.getId());
out.writeEndElement();
}
super.writeChildren(out, player, showAll, toSavedGame);
} else if ((pet = getTile().getPlayerExploredTile(player)) != null) {
if (hasSpokenToChief(player)) {
out.writeStartElement(IS_VISITED_TAG_NAME);
out.writeAttribute("player", player.getId());
out.writeEndElement();
}
if (getAlarm(player) != null) {
out.writeStartElement(ALARM_TAG_NAME);
out.writeAttribute("player", player.getId());
out.writeAttribute(VALUE_TAG, String.valueOf(getAlarm(player).getValue()));
out.writeEndElement();
}
if (pet.getMissionary() != null) {
out.writeStartElement(MISSIONARY_TAG_NAME);
pet.getMissionary().toXML(out, player, showAll, toSavedGame);
out.writeEndElement();
}
}
}
/**
* Initialize this object from an XML-representation of this object.
*
* @param in The input stream with the XML.
* @throws XMLStreamException if a problem was encountered
* during parsing.
*/
@Override
protected void readAttributes(XMLStreamReader in) throws XMLStreamException {
super.readAttributes(in);
owner.addSettlement(this);
ownedUnits.clear();
for (int i = 0; i < wantedGoods.length; i++) {
String tag = WANTED_GOODS_TAG_NAME + Integer.toString(i);
String wantedGoodsId = getAttribute(in, tag, null);
wantedGoods[i] = (wantedGoodsId == null) ? null
: getSpecification().getGoodsType(wantedGoodsId);
}
convertProgress = getAttribute(in, "convertProgress", 0);
lastTribute = getAttribute(in, "lastTribute", 0);
learnableSkill = getSpecification().getType(in, "learnableSkill", UnitType.class, null);
}
protected void readChildren(XMLStreamReader in) throws XMLStreamException {
spokenTo.clear();
alarm = new HashMap<Player, Tension>();
missionary = null;
ownedUnits.clear();
super.readChildren(in);
}
protected void readChild(XMLStreamReader in) throws XMLStreamException {
if (IS_VISITED_TAG_NAME.equals(in.getLocalName())) {
Player player = (Player)getGame().getFreeColGameObject(in.getAttributeValue(null, "player"));
spokenTo.add(player);
in.nextTag(); // close tag is always generated.
} else if (ALARM_TAG_NAME.equals(in.getLocalName())) {
Player player = (Player) getGame().getFreeColGameObject(in.getAttributeValue(null, "player"));
alarm.put(player, new Tension(getAttribute(in, VALUE_TAG, 0)));
in.nextTag(); // close element
} else if (WANTED_GOODS_TAG_NAME.equals(in.getLocalName())) {
String[] wantedGoodsID = readFromArrayElement(WANTED_GOODS_TAG_NAME,
in, new String[0]);
for (int i = 0; i < wantedGoods.length; i++) {
String goodsId = (i < wantedGoodsID.length) ? wantedGoodsID[i]
: null;
wantedGoods[i] = (goodsId == null || "".equals(goodsId)) ? null
: getSpecification().getGoodsType(goodsId);
}
} else if (MISSIONARY_TAG_NAME.equals(in.getLocalName())) {
in.nextTag();
missionary = updateFreeColGameObject(in, Unit.class);
missionary.setLocationNoUpdate(this);
in.nextTag();
} else if (UNITS_TAG_NAME.equals(in.getLocalName())) {
while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
if (in.getLocalName().equals(Unit.getXMLElementTagName())) {
Unit unit = updateFreeColGameObject(in, Unit.class);
// @compat 0.10.1
if (unit.getLocation() != this) {
logger.warning("fixing unit location");
unit.setLocation(this);
}
// end compatibility code
add(unit);
}
}
} else if (OWNED_UNITS_TAG_NAME.equals(in.getLocalName())) {
Unit unit = getFreeColGameObject(in, ID_ATTRIBUTE, Unit.class);
if (unit.getOwner() != null && !owner.owns(unit)) {
logger.warning("Error in savegame: unit " + unit.getId()
+ " does not belong to settlement " + getId());
} else {
ownedUnits.add(unit);
owner.setUnit(unit);
}
in.nextTag();
} else {
super.readChild(in);
}
}
/**
* Partial writer, 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, 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());
}
@Override
public String toString() {
StringBuilder s = new StringBuilder(getName());
s.append(" at (").append(tile.getX());
s.append(",").append(tile.getY()).append(")");
return s.toString();
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "indianSettlement".
*/
public static String getXMLElementTagName() {
return "indianSettlement";
}
}