mirror of https://github.com/FreeCol/freecol.git
1807 lines
58 KiB
Java
1807 lines
58 KiB
Java
/**
|
|
* Copyright (C) 2002-2022 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 static net.sf.freecol.common.util.CollectionUtils.accumulateToMap;
|
|
import static net.sf.freecol.common.util.CollectionUtils.any;
|
|
import static net.sf.freecol.common.util.CollectionUtils.cacheInt;
|
|
import static net.sf.freecol.common.util.CollectionUtils.count;
|
|
import static net.sf.freecol.common.util.CollectionUtils.descendingIntegerComparator;
|
|
import static net.sf.freecol.common.util.CollectionUtils.first;
|
|
import static net.sf.freecol.common.util.CollectionUtils.flatten;
|
|
import static net.sf.freecol.common.util.CollectionUtils.forEachMapEntry;
|
|
import static net.sf.freecol.common.util.CollectionUtils.isNotNull;
|
|
import static net.sf.freecol.common.util.CollectionUtils.iterable;
|
|
import static net.sf.freecol.common.util.CollectionUtils.mapEntriesByValue;
|
|
import static net.sf.freecol.common.util.CollectionUtils.maximize;
|
|
import static net.sf.freecol.common.util.CollectionUtils.sort;
|
|
import static net.sf.freecol.common.util.CollectionUtils.sum;
|
|
import static net.sf.freecol.common.util.CollectionUtils.transform;
|
|
import static net.sf.freecol.common.util.RandomUtils.getRandomMember;
|
|
import static net.sf.freecol.common.util.RandomUtils.randomInt;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map.Entry;
|
|
import java.util.Random;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.function.ToIntFunction;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
|
|
import javax.xml.stream.XMLStreamException;
|
|
|
|
import net.sf.freecol.common.i18n.Messages;
|
|
import net.sf.freecol.common.io.FreeColXMLReader;
|
|
import net.sf.freecol.common.io.FreeColXMLWriter;
|
|
import net.sf.freecol.common.model.Constants.IntegrityType;
|
|
import net.sf.freecol.common.option.GameOptions;
|
|
import net.sf.freecol.common.util.LogBuilder;
|
|
|
|
|
|
/**
|
|
* Represents an Indian settlement.
|
|
*/
|
|
public class IndianSettlement extends Settlement implements TradeLocation {
|
|
|
|
private static final Logger logger = Logger.getLogger(IndianSettlement.class.getName());
|
|
|
|
public static final String TAG = "indianSettlement";
|
|
|
|
/** The level of contact between a player and this settlement. */
|
|
public enum ContactLevel {
|
|
UNCONTACTED, // Nothing known other than location?
|
|
CONTACTED, // Name, wanted-goods now visible
|
|
VISITED, // Skill now known
|
|
SCOUTED // Scouting bonus consumed
|
|
};
|
|
|
|
|
|
/** The production fudge factor. */
|
|
public static final double NATIVE_PRODUCTION_EFFICIENCY = 0.67;
|
|
|
|
/** The maximum number of wanted goods. */
|
|
public static final int WANTED_GOODS_COUNT = 3;
|
|
|
|
/** Radius of native tales map reveal. */
|
|
public static final int TALES_RADIUS = 6;
|
|
|
|
/** 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;
|
|
|
|
/**
|
|
* Generate gifts from goods that exceed KEEP_RAW_MATERIAL +
|
|
* GIFT_THRESHOLD.
|
|
*/
|
|
public static final int GIFT_THRESHOLD = 25;
|
|
|
|
/** The minimum gift amount. */
|
|
public static final int GIFT_MINIMUM = 10;
|
|
|
|
/** The maximum gift amount. */
|
|
public static final int GIFT_MAXIMUM = 80;
|
|
|
|
|
|
/**
|
|
* 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 final List<GoodsType> wantedGoods = emptyWantedGoods();
|
|
|
|
/**
|
|
* 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 final java.util.Map<Player, ContactLevel> contactLevels
|
|
= new HashMap<>();
|
|
|
|
/** Units that belong to this settlement. */
|
|
protected final List<Unit> ownedUnits = new ArrayList<>();
|
|
|
|
/** 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;
|
|
|
|
/** The most hated nation. */
|
|
protected Player mostHated = null;
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
protected final java.util.Map<Player, Tension> alarm = new HashMap<>();
|
|
|
|
/** Cache of goods offered for sale. Do not serialize. */
|
|
private final List<Goods> forSale = new ArrayList<>();
|
|
|
|
|
|
/**
|
|
* Constructor for ServerIndianSettlement.
|
|
*
|
|
* @param game The enclosing {@code Game}.
|
|
* @param owner The {@code Player} owning this settlement.
|
|
* @param name The name for this settlement.
|
|
* @param tile The containing {@code Tile}.
|
|
*/
|
|
protected IndianSettlement(Game game, Player owner, String name,
|
|
Tile tile) {
|
|
super(game, owner, name, tile);
|
|
}
|
|
|
|
/**
|
|
* Creates a new {@code IndianSettlement} with the given
|
|
* identifier. The object should later be initialized by calling either
|
|
* {@link #readFromXML(FreeColXMLReader)}.
|
|
*
|
|
* @param game The {@code Game} in which this object belong.
|
|
* @param id The object identifier.
|
|
*/
|
|
public IndianSettlement(Game game, String id) {
|
|
super(game, id);
|
|
}
|
|
|
|
|
|
/**
|
|
* Create an empty wanted goods list.
|
|
*
|
|
* @return A list of null wanted goods.
|
|
*/
|
|
private static List<GoodsType> emptyWantedGoods() {
|
|
List<GoodsType> ret = new ArrayList<>(WANTED_GOODS_COUNT);
|
|
for (int i = 0; i < WANTED_GOODS_COUNT; i++) ret.add(null);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets a list of the units native to this settlement.
|
|
*
|
|
* @return The list of units native to this settlement.
|
|
*/
|
|
public List<Unit> getOwnedUnitList() {
|
|
synchronized (this.ownedUnits) {
|
|
return new ArrayList<>(this.ownedUnits);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the owned units list.
|
|
*
|
|
* @param ownedUnits The new owned {@code Unit} list.
|
|
*/
|
|
protected void setOwnedUnitList(List<Unit> ownedUnits) {
|
|
clearOwnedUnits();
|
|
this.ownedUnits.addAll(ownedUnits);
|
|
}
|
|
|
|
/**
|
|
* Adds the given {@code Unit} to the list of units that
|
|
* belongs to this {@code IndianSettlement}.
|
|
*
|
|
* @param unit The {@code Unit} to be added.
|
|
*/
|
|
public void addOwnedUnit(Unit unit) {
|
|
synchronized (this.ownedUnits) {
|
|
if (!this.ownedUnits.contains(unit)) {
|
|
this.ownedUnits.add(unit);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the owned units.
|
|
*/
|
|
private void clearOwnedUnits() {
|
|
synchronized (this.ownedUnits) {
|
|
this.ownedUnits.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the given {@code Unit} to the list of units that
|
|
* belongs to this {@code IndianSettlement}. Returns true if
|
|
* the Unit was removed.
|
|
*
|
|
* @param unit The {@code Unit} to be removed from the
|
|
* list of the units this {@code IndianSettlement} owns.
|
|
* @return a {@code boolean} value
|
|
*/
|
|
public boolean removeOwnedUnit(Unit unit) {
|
|
synchronized (this.ownedUnits) {
|
|
return this.ownedUnits.remove(unit);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 skill that can be learned at this settlement.
|
|
*
|
|
* @return The skill that can be learned at this settlement.
|
|
*/
|
|
public UnitType getLearnableSkill() {
|
|
return learnableSkill;
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Get a label appropriate to the current learnable skill
|
|
* and whether the requestor has visited this settlement.
|
|
*
|
|
* @param visited The visiting status.
|
|
* @return A {@code StringTemplate} describing the perceived skill.
|
|
*/
|
|
public StringTemplate getLearnableSkillLabel(boolean visited) {
|
|
return StringTemplate.key((visited)
|
|
? ((learnableSkill == null)
|
|
? "model.indianSettlement.skillNone"
|
|
: learnableSkill.getNameKey())
|
|
: "model.indianSettlement.skillUnknown");
|
|
}
|
|
|
|
/**
|
|
* Gets the missionary from this settlement.
|
|
*
|
|
* @return The missionary at this settlement, or null if there is none.
|
|
*/
|
|
public Unit getMissionary() {
|
|
return missionary;
|
|
}
|
|
|
|
/**
|
|
* Does this settlement have a missionary?
|
|
*
|
|
* @return True if there is a missionary at this settlement.
|
|
*/
|
|
public boolean hasMissionary() {
|
|
return missionary != null;
|
|
}
|
|
|
|
/**
|
|
* Does this settlement have a missionary from the given player?
|
|
*
|
|
* @param player The {@code Player} to test.
|
|
* @return True if there is a suitable missionary present.
|
|
*/
|
|
public boolean hasMissionary(Player player) {
|
|
return missionary != null && player != null && player.owns(missionary);
|
|
}
|
|
|
|
/**
|
|
* Sets the missionary for this settlement.
|
|
*
|
|
* -vis: This routine has visibility implications when enhanced
|
|
* missionaries are enabled.
|
|
* -til: This changes the tile appearance.
|
|
*
|
|
* @param missionary The missionary for this settlement.
|
|
*/
|
|
public void setMissionary(Unit missionary) {
|
|
this.missionary = missionary;
|
|
}
|
|
|
|
/**
|
|
* Get the line of sight used by a missionary at this settlement.
|
|
*
|
|
* @return The missionary line of sight.
|
|
*/
|
|
public int getMissionaryLineOfSight() {
|
|
final boolean enhanced = getSpecification()
|
|
.getBoolean(GameOptions.ENHANCED_MISSIONARIES);
|
|
return (enhanced) ? getLineOfSight() : 1;
|
|
}
|
|
|
|
/**
|
|
* Get a stream of tiles that should be visible to a missionary at
|
|
* this settlement.
|
|
*
|
|
* @return A list of {@code Tile}s.
|
|
*/
|
|
public List<Tile> getMissionaryVisibleTiles() {
|
|
return getTile().getSurroundingTiles(0, getMissionaryLineOfSight());
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
/**
|
|
* Is a given index with the range of normal wanted goods.
|
|
*
|
|
* @param index The index to test.
|
|
* @return True if the index is in range.
|
|
*/
|
|
private boolean validWantedGoodsIndex(int index) {
|
|
return 0 <= index && index < WANTED_GOODS_COUNT;
|
|
}
|
|
|
|
/**
|
|
* Gets the goods wanted by this settlement.
|
|
*
|
|
* @return The wanted goods list.
|
|
*/
|
|
public List<GoodsType> getWantedGoods() {
|
|
return this.wantedGoods;
|
|
}
|
|
|
|
/**
|
|
* Gets one of the goods wanted by this settlement.
|
|
*
|
|
* @param index Which of the goods to get.
|
|
* @return The wanted {@code GoodsType} or null if not present or
|
|
* the index is out of range.
|
|
*/
|
|
public GoodsType getWantedGoods(int index) {
|
|
return (validWantedGoodsIndex(index)) ? this.wantedGoods.get(index)
|
|
: null;
|
|
}
|
|
|
|
/**
|
|
* Sets the goods wanted by this Settlement.
|
|
*
|
|
* @param wanted The new wanted {@code GoodsType} list.
|
|
*/
|
|
public void setWantedGoods(List<GoodsType> wanted) {
|
|
final int n = wanted.size();
|
|
for (int i = 0; i < WANTED_GOODS_COUNT; i++) {
|
|
this.wantedGoods.set(i, ((i < n) ? wanted.get(i) : null));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the goods wanted by this settlement.
|
|
*
|
|
* @param index Which of the (usually 3) goods to set.
|
|
* @param type The {@code GoodsType} wanted.
|
|
*/
|
|
public void setWantedGoods(int index, GoodsType type) {
|
|
if (validWantedGoodsIndex(index)) this.wantedGoods.set(index, type);
|
|
}
|
|
|
|
/**
|
|
* Get the number of wanted goods. If any members of the array
|
|
* are null, we assume that subsequent ones are too.
|
|
*
|
|
* @return The number of wanted goods.
|
|
*/
|
|
public int getWantedGoodsCount() {
|
|
return count(this.wantedGoods, isNotNull());
|
|
}
|
|
|
|
/**
|
|
* Get a label for one of the wanted goods.
|
|
*
|
|
* @param index The index into the wanted goods.
|
|
* @param player The requesting {@code Player}.
|
|
* @return A list of a {@code StringTemplate} for the label, and
|
|
* optionally one for a tool tip.
|
|
*/
|
|
public List<StringTemplate> getWantedGoodsLabel(int index, Player player) {
|
|
StringTemplate lab = null, tip = null;
|
|
GoodsType gt;
|
|
if (hasVisited(player)) {
|
|
if ((gt = getWantedGoods(index)) != null) {
|
|
lab = StringTemplate.label("").add(Messages.nameKey(gt));
|
|
String sale = player.getLastSaleString(this, gt);
|
|
if (sale != null) {
|
|
lab.addName(" " + sale);
|
|
tip = player.getLastSaleTip(this, gt);
|
|
}
|
|
}
|
|
if (lab == null) {
|
|
lab = StringTemplate.key("model.indianSettlement.wantedGoodsNone");
|
|
}
|
|
} else {
|
|
lab = StringTemplate.name("");
|
|
}
|
|
List<StringTemplate> ret = new ArrayList<>();
|
|
ret.add(lab);
|
|
if (tip != null) ret.add(tip);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Gets the most hated nation of this settlement.
|
|
*
|
|
* @return The most hated nation.
|
|
*/
|
|
public Player getMostHated() {
|
|
return mostHated;
|
|
}
|
|
|
|
/**
|
|
* Sets the most hated nation of this settlement.
|
|
*
|
|
* -til: Changes the tile appearance.
|
|
*
|
|
* @param mostHated The new most hated nation.
|
|
*/
|
|
public void setMostHated(Player mostHated) {
|
|
this.mostHated = mostHated;
|
|
}
|
|
|
|
/**
|
|
* Get a template for the current most hated nation, depending
|
|
* whether the requestor has contacted this settlement.
|
|
*
|
|
* @param contacted The contact status.
|
|
* @return A {@code StringTemplate} describing the perceived
|
|
* most hated nation.
|
|
*/
|
|
public StringTemplate getMostHatedLabel(boolean contacted) {
|
|
return (contacted)
|
|
? ((mostHated == null)
|
|
? StringTemplate.key("model.indianSettlement.mostHatedNone")
|
|
: mostHated.getCountryLabel())
|
|
: StringTemplate.key("model.indianSettlement.mostHatedUnknown");
|
|
}
|
|
|
|
/**
|
|
* Set the contact levels.
|
|
*
|
|
* @param contactLevels The new contact level map.
|
|
*/
|
|
protected void setContactLevels(java.util.Map<Player, ContactLevel> contactLevels) {
|
|
synchronized (this.contactLevels) {
|
|
this.contactLevels.clear();
|
|
this.contactLevels.putAll(contactLevels);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the contact levels.
|
|
*/
|
|
private void clearContactLevels() {
|
|
synchronized (this.contactLevels) {
|
|
this.contactLevels.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the contact level between this settlement and a player.
|
|
*
|
|
* @param player The {@code Player} to check.
|
|
* @return The contact level.
|
|
*/
|
|
public ContactLevel getContactLevel(Player player) {
|
|
synchronized (this.contactLevels) {
|
|
ContactLevel cl = this.contactLevels.get(player);
|
|
return (cl == null) ? ContactLevel.UNCONTACTED : cl;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a contact level between this settlement and a player.
|
|
*
|
|
* @param player The contacted {@code Player}.
|
|
* @param level The new {@code ContactLevel}.
|
|
*/
|
|
private void setContactLevel(Player player, ContactLevel level) {
|
|
synchronized (this.contactLevels) {
|
|
this.contactLevels.put(player, level);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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} making contact.
|
|
* @return True if this was indeed the first contact between settlement
|
|
* and player.
|
|
*/
|
|
public boolean setContacted(Player player) {
|
|
if (!hasContacted(player)) {
|
|
setContactLevel(player, ContactLevel.CONTACTED);
|
|
initializeAlarm(player);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Has a player visited this settlement?
|
|
*
|
|
* @param player The {@code Player} to check.
|
|
* @return True if the player has contacted this settlement.
|
|
*/
|
|
public boolean hasVisited(Player player) {
|
|
return getContactLevel(player).ordinal()
|
|
>= ContactLevel.VISITED.ordinal();
|
|
}
|
|
|
|
/**
|
|
* Sets the contact level of this settlement to indicate
|
|
* that a European player has visited the settlement.
|
|
*
|
|
* @param player The visiting {@code Player}.
|
|
* @return True if this was the first time the settlement was visited
|
|
* by the player.
|
|
*/
|
|
public boolean setVisited(Player player) {
|
|
if (!hasVisited(player)) {
|
|
if (!hasContacted(player)) initializeAlarm(player);
|
|
setContactLevel(player, ContactLevel.VISITED);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Has a player has spoken with the chief of this settlement.
|
|
*
|
|
* @param player The {@code Player} to check.
|
|
* @return True if the player has visited this settlement to speak
|
|
* with the chief.
|
|
*/
|
|
public boolean hasScouted(Player player) {
|
|
return getContactLevel(player) == ContactLevel.SCOUTED;
|
|
}
|
|
|
|
/**
|
|
* Sets the contact level of this settlement to indicate
|
|
* that a European player has had a chat with the chief.
|
|
*
|
|
* @param player The visiting {@code Player}.
|
|
* @return True if this was the first time the settlement was scouted
|
|
* by the player.
|
|
*/
|
|
public boolean setScouted(Player player) {
|
|
if (!hasScouted(player)) {
|
|
if (!hasContacted(player)) initializeAlarm(player);
|
|
setContactLevel(player, ContactLevel.SCOUTED);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Has any European player spoken with the chief of this settlement.
|
|
*
|
|
* @return True if any European player has spoken with the chief.
|
|
*/
|
|
public boolean hasAnyScouted() {
|
|
synchronized (this.contactLevels) {
|
|
return any(this.contactLevels.keySet(), p -> hasScouted(p));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is this settlement worth scouting?
|
|
* That is, has it been contacted, but not scouted already, or
|
|
* visited when the "Chief contact" option is set.
|
|
*
|
|
* @param player The {@code Player} contemplating scouting.
|
|
* @return Whether it might be worth the player scouting this settlement.
|
|
*/
|
|
public boolean worthScouting(Player player) {
|
|
ContactLevel cl = getContactLevel(player);
|
|
switch (cl) {
|
|
case CONTACTED:
|
|
return true;
|
|
case VISITED:
|
|
return !getSpecification()
|
|
.getBoolean(GameOptions.SETTLEMENT_ACTIONS_CONTACT_CHIEF);
|
|
case UNCONTACTED: case SCOUTED: default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the alarm map.
|
|
*
|
|
* @return The map of player to tension.
|
|
*/
|
|
protected java.util.Map<Player, Tension> getAlarm() {
|
|
return this.alarm;
|
|
}
|
|
|
|
/**
|
|
* Set the alarm map.
|
|
*
|
|
* @param alarm The new map of {@code Player} to {@code Tension}.
|
|
*/
|
|
protected void setAlarm(java.util.Map<Player, Tension> alarm) {
|
|
clearAlarm();
|
|
synchronized (this.alarm) {
|
|
this.alarm.putAll(alarm);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the alarm level towards the given player.
|
|
*
|
|
* @param player The {@code Player} 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) {
|
|
synchronized (this.alarm) {
|
|
return this.alarm.get(player);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets alarm towards the given player.
|
|
*
|
|
* -til: Might change tile appearance through most hated state
|
|
*
|
|
* @param player The {@code Player} to set the alarm level for.
|
|
* @param newAlarm The new alarm value.
|
|
*/
|
|
public void setAlarm(Player player, Tension newAlarm) {
|
|
synchronized (this.alarm) {
|
|
this.alarm.put(player, newAlarm);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the alarm levels.
|
|
*/
|
|
private void clearAlarm() {
|
|
synchronized (this.alarm) {
|
|
this.alarm.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize the alarm at this settlement with respect to a
|
|
* player with the current national tension.
|
|
*
|
|
* @param player The {@code Player} to set the alarm level for.
|
|
*/
|
|
protected void initializeAlarm(Player player) {
|
|
Tension tension = owner.getTension(player);
|
|
setAlarm(player, new Tension(tension.getValue()));
|
|
}
|
|
|
|
/**
|
|
* Gets a message key for a short alarm message associated with the
|
|
* alarm level of this player.
|
|
*
|
|
* @param player The other {@code Player}.
|
|
* @return The alarm message key.
|
|
*/
|
|
public String getAlarmLevelKey(Player player) {
|
|
return (!player.hasContacted(owner))
|
|
? "model.indianSettlement.tension.wary"
|
|
: (!hasContacted(player))
|
|
? "model.indianSettlement.tension.unknown"
|
|
: getAlarm(player).getNameKey();
|
|
}
|
|
|
|
/**
|
|
* Get the current goods offered for sale.
|
|
*
|
|
* @return A list of {@code Goods} for sale.
|
|
*/
|
|
public List<Goods> getGoodsForSale() {
|
|
return this.forSale;
|
|
}
|
|
|
|
/**
|
|
* Set the current goods offered for sale.
|
|
*
|
|
* @param forSale A new list of {@code Goods} for sale.
|
|
*/
|
|
public void setGoodsForSale(List<Goods> forSale) {
|
|
this.forSale.clear();
|
|
this.forSale.addAll(forSale);
|
|
}
|
|
|
|
/**
|
|
* 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} 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.hasGoodsCargo();
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the amount of gold this {@code IndianSettlment}
|
|
* is willing to pay for the given {@code Goods}.
|
|
*
|
|
* It is only meaningful to call this method from the
|
|
* server, since the settlement's {@link GoodsContainer}
|
|
* is hidden from the clients.
|
|
*
|
|
* @param <T> The base type of the goods.
|
|
* @param goods The goods to price.
|
|
* @return The price.
|
|
*/
|
|
public <T extends AbstractGoods> int getPriceToBuy(T goods) {
|
|
return getPriceToBuy(goods.getType(), goods.getAmount());
|
|
}
|
|
|
|
/**
|
|
* Gets the amount of gold this {@code IndianSettlment}
|
|
* is willing to pay for the given {@code Goods}.
|
|
*
|
|
* 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.
|
|
*
|
|
* FIXME: this is rancid with magic numbers.
|
|
*
|
|
* @param type The type of {@code Goods} to price.
|
|
* @param amount The amount of {@code Goods} to price.
|
|
* @return The price.
|
|
*/
|
|
public int getPriceToBuy(GoodsType type, int amount) {
|
|
if (amount > GoodsContainer.CARGO_SIZE) {
|
|
throw new RuntimeException("Amount " + amount
|
|
+ " > " + GoodsContainer.CARGO_SIZE);
|
|
}
|
|
|
|
int price = 0;
|
|
if (type.getMilitary()) {
|
|
// 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 == getWantedGoods(0)) ? 150
|
|
: (type == getWantedGoods(1)) ? 125
|
|
: (type == getWantedGoods(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.getInputType();
|
|
if (rawType != null) {
|
|
int rawProduction = getMaximumProduction(rawType);
|
|
// Pretend that we can "easily" produce a certain amount of the
|
|
// manufactured goods
|
|
int fakeProduction = (rawProduction < 5) ? 10 * rawProduction
|
|
: (rawProduction < 10) ? 5 * rawProduction + 25
|
|
: (rawProduction < 20) ? 2 * rawProduction + 55
|
|
: 100;
|
|
fakeProduction /= 2;
|
|
// Pretend that we have actually done so, in proportion to
|
|
// the available space
|
|
current += fakeProduction * Math.max(0, capacity - current) / capacity;
|
|
} 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.
|
|
return (unitPrice < 0) ? 0 : valued * unitPrice;
|
|
}
|
|
|
|
/**
|
|
* Calculates how much of the given goods type this settlement
|
|
* wants and should retain.
|
|
*
|
|
* @param type The {@code GoodsType}.
|
|
* @return The amount of goods wanted.
|
|
*/
|
|
protected int getWantedGoodsAmount(GoodsType type) {
|
|
if (getUnitCount() <= 0) return 0;
|
|
|
|
final Specification spec = getSpecification();
|
|
final UnitType unitType = getFirstUnit().getType();
|
|
final List<Role> militaryRoles = Role.getAvailableRoles(getOwner(),
|
|
unitType, spec.getMilitaryRolesList());
|
|
|
|
if (type.getMilitary()) { // Retain enough goods to fully arm
|
|
return sum(getOwnedUnitList(),
|
|
u -> !militaryRoles.contains(u.getRole()),
|
|
u -> AbstractGoods.getCount(type,
|
|
u.getGoodsDifference(first(militaryRoles), 1)));
|
|
}
|
|
|
|
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}
|
|
* is willing to sell the given {@code Goods} for.
|
|
*
|
|
* It is only meaningful to call this method from the
|
|
* server, since the settlement's {@link GoodsContainer}
|
|
* is hidden from the clients.
|
|
*
|
|
* @param <T> The base type of the goods.
|
|
* @param goods The goods to price.
|
|
* @return The price.
|
|
*/
|
|
public <T extends AbstractGoods> int getPriceToSell(T goods) {
|
|
return getPriceToSell(goods.getType(), goods.getAmount());
|
|
}
|
|
|
|
/**
|
|
* Gets the amount of gold this {@code IndianSettlment}
|
|
* is willing to sell the given {@code Goods} 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} to price.
|
|
* @param amount The amount of {@code Goods} to price.
|
|
* @return The price.
|
|
*/
|
|
public int getPriceToSell(GoodsType type, int amount) {
|
|
if (amount > GoodsContainer.CARGO_SIZE) {
|
|
throw new RuntimeException("Amount " + 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%
|
|
// - add another point if the goods can not be produced
|
|
int price = amount + Math.max(0, 11 * getPriceToBuy(type, amount) / 10);
|
|
if (type.getMilitary()) {
|
|
price = Math.max(price, amount * full * 2);
|
|
} else if (type.isTradeGoods()) {
|
|
price = Math.max(price, 150 * amount * full / 100);
|
|
} else {
|
|
GoodsType raw = type.getInputType();
|
|
if (raw != null && getMaximumProduction(raw) == 0) {
|
|
price += amount;
|
|
}
|
|
}
|
|
return price;
|
|
}
|
|
|
|
/**
|
|
* Will this settlement sell a type of goods.
|
|
* Placeholder until we have a spec-configured blacklist.
|
|
*
|
|
* @param type The {@code GoodsType} 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.
|
|
*
|
|
* Sell new world goods first, then by decreasing price, then
|
|
* decreasing amount.
|
|
*
|
|
* @param unit An optional {@code Unit} that is trading.
|
|
* @return A list of goods to sell.
|
|
*/
|
|
public List<Goods> getSellGoods(Unit unit) {
|
|
// Collect all the candidate goods
|
|
List<Goods> sellGoods = transform(getCompactGoodsList(),
|
|
g2 -> willSell(g2.getType()));
|
|
List<Goods> result = new ArrayList<>(sellGoods.size());
|
|
for (Goods g : sellGoods) {
|
|
int amount = g.getAmount();
|
|
int retain = getWantedGoodsAmount(g.getType());
|
|
if (retain >= amount) continue;
|
|
amount -= retain;
|
|
if (unit != null) {
|
|
amount = Math.round(applyModifiers((float)amount,
|
|
getGame().getTurn(),
|
|
unit.getModifiers(Modifier.TRADE_VOLUME_PENALTY)));
|
|
}
|
|
if (amount < TRADE_MINIMUM_SIZE) continue;
|
|
if (amount > GoodsContainer.CARGO_SIZE) {
|
|
amount = GoodsContainer.CARGO_SIZE;
|
|
}
|
|
result.add(new Goods(getGame(), this, g.getType(), amount));
|
|
}
|
|
|
|
// Sort and truncate to limit
|
|
final Comparator<Goods> salePriceComparator
|
|
= Comparator.comparingInt((Goods g)
|
|
-> getPriceToSell(g.getType(), g.getAmount()))
|
|
.reversed();
|
|
final Comparator<Goods> exportGoodsComparator
|
|
= Comparator.comparingInt((Goods g) ->
|
|
(g.getType().isNewWorldGoodsType()) ? 0 : 1)
|
|
.thenComparing(salePriceComparator)
|
|
.thenComparing(AbstractGoods.descendingAmountComparator);
|
|
return sort(result, exportGoodsComparator);
|
|
}
|
|
|
|
|
|
/**
|
|
* Allows spread of horses and arms between settlements
|
|
*
|
|
* @param is The other {@code IndianSettlement} to trade with.
|
|
*/
|
|
public void tradeGoodsWithSettlement(IndianSettlement is) {
|
|
final Specification spec = getSpecification();
|
|
for (GoodsType gt : transform(spec.getGoodsTypeList(),
|
|
GoodsType::getMilitary)) {
|
|
int goodsInStock = getGoodsCount(gt);
|
|
if (goodsInStock <= 50) continue; // FIXME: magic number
|
|
int goodsTraded = goodsInStock / 2;
|
|
is.addGoods(gt, goodsTraded);
|
|
this.removeGoods(gt, 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) {
|
|
return sum(getTile().getSurroundingTiles(0, getRadius()),
|
|
t -> t.getOwningSettlement() == null
|
|
|| t.getOwningSettlement() == this,
|
|
// FIXME: make unitType brave
|
|
t -> t.getPotentialProduction(goodsType, null));
|
|
}
|
|
|
|
|
|
/**
|
|
* 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() {
|
|
final Specification spec = getSpecification();
|
|
final Function<GoodsType, GoodsType> identity
|
|
= Function.<GoodsType>identity();
|
|
final java.util.Map<GoodsType, Integer> prices
|
|
= transform(spec.getGoodsTypeList(),
|
|
gt -> !gt.getMilitary() && gt.isStorable(),
|
|
identity,
|
|
Collectors.toMap(identity,
|
|
gt -> getNormalGoodsPriceToBuy(gt,
|
|
GoodsContainer.CARGO_SIZE)));
|
|
|
|
int wantedIndex = 0;
|
|
for (Entry<GoodsType, Integer> e
|
|
: mapEntriesByValue(prices, descendingIntegerComparator)) {
|
|
if (!validWantedGoodsIndex(wantedIndex)) break;
|
|
if (e.getValue() <= GoodsContainer.CARGO_SIZE
|
|
* TRADE_MINIMUM_PRICE) break;
|
|
setWantedGoods(wantedIndex, e.getKey());
|
|
wantedIndex++;
|
|
}
|
|
for (; wantedIndex < WANTED_GOODS_COUNT; wantedIndex++) {
|
|
setWantedGoods(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} to manufacture, or null if
|
|
* none suitable.
|
|
*/
|
|
private GoodsType goodsToMake() {
|
|
final ToIntFunction<GoodsType> deficit = cacheInt(gt ->
|
|
getWantedGoodsAmount(gt) - getGoodsCount(gt));
|
|
final Predicate<GoodsType> goodsPred = gt ->
|
|
gt.isRawMaterial()
|
|
&& gt.getOutputType() != null
|
|
&& !gt.getOutputType().isBreedable()
|
|
&& gt.getOutputType().isStorable()
|
|
&& deficit.applyAsInt(gt) < 0
|
|
&& deficit.applyAsInt(gt.getOutputType()) > 0;
|
|
final Comparator<GoodsType> comp = Comparator.comparingInt(deficit);
|
|
return maximize(getSpecification().getGoodsTypeList(), goodsPred, comp);
|
|
}
|
|
|
|
/**
|
|
* Gets a random goods gift from this settlement.
|
|
*
|
|
* @param random A pseudo random number source.
|
|
* @return A random goods gift, or null if none found.
|
|
*/
|
|
public Goods getRandomGift(Random random) {
|
|
final Specification spec = getSpecification();
|
|
final Predicate<GoodsType> goodsPred = gt ->
|
|
getGoodsCount(gt) >= GIFT_THRESHOLD + KEEP_RAW_MATERIAL;
|
|
final Function<GoodsType, Goods> newGoodsMapper = gt ->
|
|
new Goods(getGame(), this, gt,
|
|
Math.min(randomInt(logger, "Gift amount", random,
|
|
getGoodsCount(gt) - KEEP_RAW_MATERIAL
|
|
- GIFT_MINIMUM) + GIFT_MINIMUM,
|
|
GIFT_MAXIMUM));
|
|
return getRandomMember(logger, "Gift type",
|
|
transform(spec.getNewWorldGoodsTypeList(),
|
|
goodsPred, newGoodsMapper),
|
|
random);
|
|
}
|
|
|
|
/**
|
|
* Add some initial goods to a newly generated settlement.
|
|
* After all, they have been here for some time.
|
|
*
|
|
* @param random A pseudo-random number source.
|
|
*/
|
|
public void addRandomGoods(Random random) {
|
|
HashMap<GoodsType, Integer> goodsMap = new HashMap<>();
|
|
for (AbstractGoods ag : iterable(flatten(getOwnedTiles(),
|
|
t -> t.getSortedAutoPotential().stream()))) {
|
|
accumulateToMap(goodsMap, ag.getType().getStoredAs(),
|
|
ag.getAmount(), (a, b) -> a + b);
|
|
}
|
|
double d = randomInt(logger, "Goods at " + getName(), random, 10)
|
|
* 0.1 + 1.0;
|
|
forEachMapEntry(goodsMap, e -> {
|
|
int i = e.getValue();
|
|
if (!e.getKey().isFoodType()) i = (int)Math.round(d * e.getValue());
|
|
i = Math.min(i, GoodsContainer.CARGO_SIZE);
|
|
if (i > 0) addGoods(e.getKey(), i);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the number of braves expected to be present for the settlement
|
|
* not to be "badly defended".
|
|
*
|
|
* @return The required defender number.
|
|
*/
|
|
public int getRequiredDefenders() {
|
|
return getType().getMinimumSize() - 1;
|
|
}
|
|
|
|
|
|
// Override FreeColGameObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public void disposeResources() {
|
|
// Orphan the units whose home settlement this is.
|
|
while (!ownedUnits.isEmpty()) {
|
|
ownedUnits.remove(0).changeHomeIndianSettlement(null);
|
|
}
|
|
super.disposeResources();
|
|
}
|
|
|
|
|
|
// Interface Location (from Settlement via GoodsLocation via UnitLocation)
|
|
// Inherits
|
|
// FreeColObject.getId()
|
|
// Settlement.getTile
|
|
// Settlement.getLocationLabel
|
|
// GoodsLocation.remove
|
|
// GoodsLocation.contains
|
|
// UnitLocation.canAdd
|
|
// UnitLocation.getUnitCount
|
|
// UnitLocation.getUnits
|
|
// UnitLocation.getUnitList
|
|
// Settlement.getSettlement
|
|
// final Settlement.getRank
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public StringTemplate getLocationLabelFor(Player player) {
|
|
return (player == null || hasContacted(player))
|
|
? StringTemplate.name(getName())
|
|
: StringTemplate.key("model.indianSettlement.nameUnknown");
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean add(Locatable locatable) {
|
|
boolean result = super.add(locatable);
|
|
if (result && locatable instanceof Unit) {
|
|
Unit indian = (Unit)locatable;
|
|
if (indian.getHomeIndianSettlement() == null) {
|
|
// Adopt homeless Indians
|
|
indian.changeHomeIndianSettlement(this);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public final IndianSettlement getIndianSettlement() {
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Location up() {
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public String toShortString() {
|
|
return getName();
|
|
}
|
|
|
|
|
|
// UnitLocation
|
|
// Inherits
|
|
// UnitLocation.getSpaceTaken
|
|
// UnitLocation.moveToFront
|
|
// UnitLocation.clearUnitList
|
|
// Settlement.getNoAddReason
|
|
// UnitLocation.getUnitCapacity
|
|
|
|
|
|
// GoodsLocation
|
|
// Inherits
|
|
// GoodsLocation.addGoods
|
|
// GoodsLocation.removeGoods
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public void invalidateCache() {}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getGoodsCapacity() {
|
|
return getType().getWarehouseCapacity();
|
|
}
|
|
|
|
|
|
// Settlement
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public Unit getDefendingUnit(Unit attacker) {
|
|
Unit defender = null;
|
|
double defencePower = -1.0;
|
|
for (Unit nextUnit : getUnitList()) {
|
|
double unitPower = attacker.getGame().getCombatModel()
|
|
.getDefencePower(attacker, nextUnit);
|
|
if (Unit.betterDefender(defender, defencePower,
|
|
nextUnit, unitPower)) {
|
|
defender = nextUnit;
|
|
defencePower = unitPower;
|
|
}
|
|
}
|
|
return defender;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public double getDefenceRatio() {
|
|
return getUnitCount() * 2.0 / (getType().getMinimumSize()
|
|
+ getType().getMaximumSize());
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public boolean isBadlyDefended() {
|
|
return getUnitCount() < getRequiredDefenders();
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public RandomRange getPlunderRange(Unit attacker) {
|
|
return getType().getPlunderRange(attacker);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getSonsOfLiberty() {
|
|
// Native settlements do not generate SoL.
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getUpkeep() {
|
|
// Native settlements do not require upkeep.
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getTotalProductionOf(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 tiles = 0;
|
|
int potential = 0;
|
|
for (Tile wt : transform(getOwnedTiles(),
|
|
t -> t != getTile() && !t.isOccupied())) {
|
|
// FIXME: make unitType brave
|
|
potential += wt.getPotentialProduction(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 = (int)(potential * (double)getUnitCount() / tiles);
|
|
}
|
|
|
|
// Raw production is too generous, apply a fudge factor to reduce it
|
|
// a bit for the non-food cases.
|
|
if (!type.isFoodType()) {
|
|
potential = (int)Math.round(potential
|
|
* NATIVE_PRODUCTION_EFFICIENCY);
|
|
}
|
|
|
|
// But always add full potential of the center tile.
|
|
potential += getTile().getPotentialProduction(type, null);
|
|
return potential;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public boolean hasContacted(Player player) {
|
|
return player != null
|
|
&& (player.isIndian()
|
|
|| getContactLevel(player) != ContactLevel.UNCONTACTED);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public StringTemplate getAlarmLevelLabel(Player player) {
|
|
String key = (!player.hasContacted(owner))
|
|
? "model.indianSettlement.tension.wary"
|
|
: (!hasContacted(player))
|
|
? "model.indianSettlement.tension.unknown"
|
|
: "model.indianSettlement." + getAlarm(player).getKey();
|
|
return StringTemplate.template(key)
|
|
.addStringTemplate("%nation%", getOwner().getNationLabel());
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines the value of a potential attack on a {@code IndianSettlement}
|
|
*
|
|
* @param value The previously calculated input value from
|
|
* {@link net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission
|
|
* #scoreSettlementPath(AIUnit, PathNode, Settlement)}
|
|
* @param unit The {@code AIUnit} to check
|
|
* @return The calculated value
|
|
*/
|
|
@Override
|
|
public int calculateSettlementValue(int value, Unit unit) {
|
|
// Favour the most hostile settlements
|
|
Tension tension = this.getAlarm(unit.getOwner());
|
|
if (tension != null) value += tension.getValue() / 2;
|
|
return value;
|
|
}
|
|
|
|
|
|
// Interface TradeLocation
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getAvailableGoodsCount(GoodsType goodsType) {
|
|
return getGoodsCount(goodsType);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getExportAmount(GoodsType goodsType, int turns) {
|
|
int present = Math.max(0, getGoodsCount(goodsType)
|
|
+ turns * getTotalProductionOf(goodsType));
|
|
int wanted = getWantedGoodsAmount(goodsType);
|
|
return present - wanted;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int getImportAmount(GoodsType goodsType, int turns) {
|
|
if (goodsType.limitIgnored()) return Integer.MAX_VALUE;
|
|
|
|
int present = Math.max(0, getGoodsCount(goodsType)
|
|
- turns * getTotalProductionOf(goodsType));
|
|
int capacity = getWarehouseCapacity();
|
|
return capacity - present;
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public String getLocationName(TradeLocation tradeLocation) {
|
|
return ((IndianSettlement) tradeLocation).getName();
|
|
}
|
|
|
|
|
|
// Override FreeColGameObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) {
|
|
IntegrityType result = super.checkIntegrity(fix, lb);
|
|
final Player owner = getOwner();
|
|
if (owner != null) {
|
|
for (Unit u : getOwnedUnitList()) {
|
|
if (u.getOwner() != owner) {
|
|
if (fix) {
|
|
lb.add("\n Owned unit with wrong owner reassigned: ",
|
|
u.getId());
|
|
result = result.fix();
|
|
} else {
|
|
lb.add("\n Owned unit with wrong owner: ", u.getId());
|
|
result = result.fail();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
// Override FreeColObject
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public <T extends FreeColObject> boolean copyIn(T other) {
|
|
IndianSettlement o = copyInCast(other, IndianSettlement.class);
|
|
if (o == null || !super.copyIn(o)) return false;
|
|
final Game game = getGame();
|
|
this.learnableSkill = o.getLearnableSkill();
|
|
this.setWantedGoods(o.getWantedGoods());
|
|
synchronized (this.contactLevels) {
|
|
this.contactLevels.clear();
|
|
for (Entry<Player, ContactLevel> e : o.contactLevels.entrySet()) {
|
|
this.contactLevels.put(game.updateRef(e.getKey()), e.getValue());
|
|
}
|
|
}
|
|
this.setOwnedUnitList(game.updateRef(o.getOwnedUnitList()));
|
|
this.missionary = game.update(o.getMissionary(), false);
|
|
this.convertProgress = o.getConvertProgress();
|
|
this.lastTribute = o.getLastTribute();
|
|
this.mostHated = o.getMostHated();
|
|
this.setAlarm(o.getAlarm());
|
|
this.setGoodsForSale(o.getGoodsForSale());
|
|
return true;
|
|
}
|
|
|
|
|
|
// Serialization
|
|
|
|
private static final String ALARM_TAG = "alarm";
|
|
private static final String CONTACT_LEVEL_TAG = "contactLevel";
|
|
private static final String CONVERT_PROGRESS_TAG = "convertProgress";
|
|
private static final String LAST_TRIBUTE_TAG = "lastTribute";
|
|
private static final String LEVEL_TAG = "level";
|
|
private static final String MISSIONARY_TAG = "missionary";
|
|
private static final String MOST_HATED_TAG = "mostHated";
|
|
private static final String NAME_TAG = "name";
|
|
private static final String OWNED_UNITS_TAG = "ownedUnits";
|
|
private static final String PLAYER_TAG = "player";
|
|
// Public for now while 0.10.7 backward compatibility code in Tile
|
|
// for PlayerExploredTile needs to check these.
|
|
public static final String LEARNABLE_SKILL_TAG = "learnableSkill";
|
|
public static final String WANTED_GOODS_TAG = "wantedGoods";
|
|
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
|
|
super.writeAttributes(xw);
|
|
final ContactLevel cl = getContactLevel(xw.getClientPlayer());
|
|
final boolean full = xw.validFor(getOwner());
|
|
|
|
// Delegated from Settlement
|
|
String name = getName();
|
|
if (name != null) {
|
|
xw.writeAttribute(NAME_TAG, name);
|
|
}
|
|
|
|
if (full) { // Server internal fields only
|
|
xw.writeAttribute(LAST_TRIBUTE_TAG, lastTribute);
|
|
|
|
xw.writeAttribute(CONVERT_PROGRESS_TAG, convertProgress);
|
|
}
|
|
|
|
if (full // Other fields visible from visiting
|
|
|| cl == ContactLevel.SCOUTED || cl == ContactLevel.VISITED) {
|
|
|
|
if (learnableSkill != null) {
|
|
xw.writeAttribute(LEARNABLE_SKILL_TAG, learnableSkill);
|
|
}
|
|
|
|
for (int i = 0; i < WANTED_GOODS_COUNT; i++) {
|
|
GoodsType gt = getWantedGoods(i);
|
|
if (gt != null) xw.writeAttribute(WANTED_GOODS_TAG + i, gt);
|
|
}
|
|
|
|
final Player hated = getMostHated();
|
|
if (hated != null) xw.writeAttribute(MOST_HATED_TAG, hated);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
|
|
super.writeChildren(xw);
|
|
|
|
if (missionary != null) {
|
|
xw.writeStartElement(MISSIONARY_TAG);
|
|
|
|
missionary.toXML(xw);
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
|
|
if (xw.validFor(getOwner())) {
|
|
|
|
for (Player p : sort(this.contactLevels.keySet())) {
|
|
xw.writeStartElement(CONTACT_LEVEL_TAG);
|
|
|
|
xw.writeAttribute(LEVEL_TAG, contactLevels.get(p));
|
|
|
|
xw.writeAttribute(PLAYER_TAG, p);
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
|
|
for (Player p : sort(this.alarm.keySet())) {
|
|
xw.writeStartElement(ALARM_TAG);
|
|
|
|
xw.writeAttribute(PLAYER_TAG, p);
|
|
|
|
xw.writeAttribute(VALUE_TAG, getAlarm(p).getValue());
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
|
|
for (Unit unit : sort(this.ownedUnits)) {
|
|
xw.writeStartElement(OWNED_UNITS_TAG);
|
|
|
|
xw.writeAttribute(ID_ATTRIBUTE_TAG, unit);
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
|
|
} else {
|
|
final Player client = xw.getClientPlayer();
|
|
final ContactLevel cl = getContactLevel(client);
|
|
|
|
if (cl != null) {
|
|
xw.writeStartElement(CONTACT_LEVEL_TAG);
|
|
|
|
xw.writeAttribute(LEVEL_TAG, cl);
|
|
|
|
xw.writeAttribute(PLAYER_TAG, client);
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
|
|
final Tension alarm = getAlarm(client);
|
|
if (alarm != null) {
|
|
xw.writeStartElement(ALARM_TAG);
|
|
|
|
xw.writeAttribute(PLAYER_TAG, client);
|
|
|
|
xw.writeAttribute(VALUE_TAG, alarm.getValue());
|
|
|
|
xw.writeEndElement();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
|
|
super.readAttributes(xr);
|
|
|
|
final Specification spec = getSpecification();
|
|
|
|
lastTribute = xr.getAttribute(LAST_TRIBUTE_TAG, 0);
|
|
|
|
convertProgress = xr.getAttribute(CONVERT_PROGRESS_TAG, 0);
|
|
|
|
learnableSkill = xr.getType(spec, LEARNABLE_SKILL_TAG,
|
|
UnitType.class, (UnitType)null);
|
|
|
|
mostHated = xr.findFreeColGameObject(getGame(), MOST_HATED_TAG,
|
|
Player.class, (Player)null, false);
|
|
|
|
for (int i = 0; i < WANTED_GOODS_COUNT; i++) {
|
|
setWantedGoods(i, xr.getType(spec, WANTED_GOODS_TAG + i,
|
|
GoodsType.class, (GoodsType)null));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readChildren(FreeColXMLReader xr) throws XMLStreamException {
|
|
// Clear containers.
|
|
clearContactLevels();
|
|
clearAlarm();
|
|
missionary = null;
|
|
clearOwnedUnits();
|
|
|
|
super.readChildren(xr);
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
|
|
final Game game = getGame();
|
|
final String tag = xr.getLocalName();
|
|
|
|
if (ALARM_TAG.equals(tag)) {
|
|
Player player = xr.findFreeColGameObject(game, PLAYER_TAG,
|
|
Player.class, (Player)null, true);
|
|
setAlarm(player, new Tension(xr.getAttribute(VALUE_TAG, 0)));
|
|
xr.closeTag(ALARM_TAG);
|
|
|
|
} else if (CONTACT_LEVEL_TAG.equals(tag)) {
|
|
ContactLevel cl = xr.getAttribute(LEVEL_TAG,
|
|
ContactLevel.class, ContactLevel.UNCONTACTED);
|
|
Player player = xr.findFreeColGameObject(game, PLAYER_TAG,
|
|
Player.class, (Player)null, true);
|
|
setContactLevel(player, cl);
|
|
xr.closeTag(CONTACT_LEVEL_TAG);
|
|
|
|
} else if (MISSIONARY_TAG.equals(tag)) {
|
|
xr.nextTag();
|
|
missionary = xr.readFreeColObject(game, Unit.class);
|
|
missionary.setLocationNoUpdate(this);
|
|
xr.closeTag(MISSIONARY_TAG);
|
|
|
|
} else if (OWNED_UNITS_TAG.equals(tag)) {
|
|
Unit unit = xr.makeFreeColObject(game, ID_ATTRIBUTE_TAG,
|
|
Unit.class, true);
|
|
addOwnedUnit(unit);
|
|
xr.closeTag(OWNED_UNITS_TAG);
|
|
|
|
} else {
|
|
super.readChild(xr);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public String getXMLTagName() { return TAG; }
|
|
|
|
|
|
// Override Object
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
StringBuilder sb = new StringBuilder(64);
|
|
String name = getName();
|
|
sb.append((name == null) ? "NONAME" : name);
|
|
Tile tile = getTile();
|
|
if (tile != null) sb.append(" at (").append(tile.getX())
|
|
.append(',').append(tile.getY()).append(')');
|
|
return sb.toString();
|
|
}
|
|
}
|