freecol/src/net/sf/freecol/common/model/TileType.java

741 lines
24 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.alwaysTrue;
import static net.sf.freecol.common.util.CollectionUtils.flatten;
import static net.sf.freecol.common.util.CollectionUtils.iterable;
import static net.sf.freecol.common.util.CollectionUtils.matchKey;
import static net.sf.freecol.common.util.CollectionUtils.transform;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import javax.xml.stream.XMLStreamException;
import net.sf.freecol.common.io.FreeColXMLReader;
import net.sf.freecol.common.io.FreeColXMLWriter;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.common.util.RandomChoice;
/**
* The types of tiles.
*/
public final class TileType extends FreeColSpecObjectType
implements BaseProduction {
public static final String TAG = "tile-type";
public enum RangeType { HUMIDITY, TEMPERATURE, ALTITUDE };
public static final int HILLS_ALTITUDE = 15;
public static final int MOUNTAINS_ALTITUDE = 25;
/**
* Use these tile types only for "land maps", i.e. maps that only
* distinguish water and land.
*/
public static final TileType WATER = new TileType("WATER", true);
public static final TileType LAND = new TileType("LAND", false);
/** Is this a forested tile? */
private boolean forest;
/** Is this a water tile? */
private boolean water;
/** Can this tile be settled? */
private boolean canSettle;
/** Whether this TileType is connected to Europe. */
private boolean connected;
/** Is this elevated terrain? */
private boolean elevation;
/** The base movement cost for this tile type. */
private int basicMoveCost;
/** The base work turns for this tile type. */
private int basicWorkTurns;
/** The humidity range for this tile type. */
private final int[] humidity = new int[2];
/** The temperature range for this tile type. */
private final int[] temperature = new int[2];
/** The altitude range for this tile type. */
private final int[] altitude = new int[2];
/** The resource types that are valid for this tile type. */
private List<RandomChoice<ResourceType>> resourceTypes = null;
/** The disasters that may strike this type of tile. */
private List<RandomChoice<Disaster>> disasters = null;
/**
* The possible production types of this tile type. This includes
* the production types available if a tile of this type is a
* colony center tile.
*/
private final List<ProductionType> productionTypes = new ArrayList<>();
/**
* Create a new tile type.
*
* @param id The object identifier.
* @param specification The {@code Specification} to refer to.
*/
public TileType(String id, Specification specification) {
super(id, specification);
}
/**
* Creates a new {@code TileType} instance. This constructor
* is used to create the "virtual" tile types {@code LAND}
* and {@code WATER}, which are intended to simplify map
* loading.
*
* @param id The object identifier.
* @param water True if this is a water tile.
*/
private TileType(String id, boolean water) {
super(id, null);
this.water = water;
}
/**
* Is this tile type forested?
*
* @return True if this is a forested tile type.
*/
public boolean isForested() {
return forest;
}
/**
* Is this a water tile type?
*
* @return True if this is a water tile type.
*/
public boolean isWater() {
return water;
}
/**
* Can this tile type be settled?
*
* @return True if this is a settleable tile type.
*/
public boolean canSettle() {
return canSettle;
}
/**
* Is this tile type connected to the high seas, by definition.
*
* @return True if the tile type is inherently connected to the
* high seas.
*/
public boolean isHighSeasConnected() {
return connected;
}
/**
* Is this tile type directly connected to the high seas, that is,
* a unit on a tile of this type can move immediately to the high
* seas.
*
* @return True if the tile type is directly connected.
*/
public boolean isDirectlyHighSeasConnected() {
return hasAbility(Ability.MOVE_TO_EUROPE);
}
/**
* Is this an elevated tile type?
*
* @return True if this is an elevated tile type.
*/
public boolean isElevation() {
return elevation;
}
/**
* Gets the basic movement cost through this tile type.
*
* @return The basic movement cost.
*/
public int getBasicMoveCost() {
return basicMoveCost;
}
/**
* Checks if this {@code TileType} should be considered a hill when
* generating the map.
*/
public boolean isHills() {
return altitude[0] <= HILLS_ALTITUDE && altitude[1] >= HILLS_ALTITUDE;
}
/**
* Checks if this {@code TileType} should be considered a mountain when
* generating the map.
*/
public boolean isMountains() {
return altitude[0] <= MOUNTAINS_ALTITUDE && altitude[1] >= MOUNTAINS_ALTITUDE;
}
/**
* Gets the basic work turns to build an improvement on this tile type.
*
* @return The basic work turns.
*/
public int getBasicWorkTurns() {
return basicWorkTurns;
}
protected int getHumidity(int i) {
return this.humidity[i];
}
protected void setHumidity(int i, int value) {
this.humidity[i] = value;
}
protected int getTemperature(int i) {
return this.temperature[i];
}
protected void setTemperature(int i, int value) {
this.temperature[i] = value;
}
protected int getAltitude(int i) {
return this.altitude[i];
}
protected void setAltitude(int i, int value) {
this.altitude[i] = value;
}
/**
* Is this tile type suitable for a given range type value.
*
* @param rangeType The {@code RangeType} to test.
* @param value The value to check.
* @return True if the tile type meets the range limits.
*/
public boolean withinRange(RangeType rangeType, int value) {
switch (rangeType) {
case HUMIDITY:
return humidity[0] <= value && value <= humidity[1];
case TEMPERATURE:
return temperature[0] <= value && value <= temperature[1];
case ALTITUDE:
return altitude[0] <= value && value <= altitude[1];
default:
break;
}
return false;
}
/**
* Gets the resources that can be placed on this tile type.
*
* @return A weighted list of resource types.
*/
public List<RandomChoice<ResourceType>> getResourceTypes() {
if (this.resourceTypes == null) this.resourceTypes = new ArrayList<>();
return this.resourceTypes;
}
/**
* Set the resource types list.
*
* @param resourceTypes The list of resource type choices.
*/
protected void setResourceTypes(List<RandomChoice<ResourceType>> resourceTypes) {
if (this.resourceTypes == null) {
this.resourceTypes = new ArrayList<>();
} else {
this.resourceTypes.clear();
}
this.resourceTypes.addAll(resourceTypes);
}
/**
* Gets the resource types that can be found on this tile type.
*
* @return A list of {@code ResourceType}s.
*/
public List<ResourceType> getResourceTypeValues() {
return transform(getResourceTypes(), alwaysTrue(),
RandomChoice::getObject);
}
/**
* Add a resource type.
*
* @param type The {@code ResourceType} to add.
* @param prob The percentage probability of the resource being
* present.
*/
private void addResourceType(ResourceType type, int prob) {
if (this.resourceTypes == null) this.resourceTypes = new ArrayList<>();
this.resourceTypes.add(new RandomChoice<>(type, prob));
}
/**
* Can this tile type contain a specified resource type?
*
* @param resourceType The {@code ResourceType} to test.
* @return True if the {@code ResourceType} is compatible.
*/
public boolean canHaveResourceType(ResourceType resourceType) {
return getResourceTypeValues().contains(resourceType);
}
/**
* Get the disaster choices.
*
* @return A list of {@code Disaster} choices.
*/
protected List<RandomChoice<Disaster>> getDisasters() {
return this.disasters;
}
/**
* Set the disaster choices.
*
* @param disasters The new {@code Disaster} choice list.
*/
protected void setDisasters(List<RandomChoice<Disaster>> disasters) {
if (this.disasters == null) {
this.disasters = new ArrayList<>();
} else {
this.disasters.clear();
}
this.disasters.addAll(disasters);
}
/**
* Gets the natural disasters than can strike this tile type.
*
* @return A stream of {@code Disaster} choices.
*/
public Stream<RandomChoice<Disaster>> getDisasterChoices() {
return (this.disasters == null)
? Stream.<RandomChoice<Disaster>>empty()
: disasters.stream();
}
/**
* Add a disaster.
*
* @param disaster The {@code Disaster} to add.
* @param probability The probability of the disaster.
*/
private void addDisaster(Disaster disaster, int probability) {
if (disasters == null) disasters = new ArrayList<>();
disasters.add(new RandomChoice<>(disaster, probability));
}
/**
* Get the production type list.
*
* @return The {@code ProductionType} list.
*/
protected List<ProductionType> getProductionTypes() {
return this.productionTypes;
}
/**
* Set the production type list.
*
* @param productionTypes The new {@code ProductionType} list.
*/
protected void setProductionTypes(List<ProductionType> productionTypes) {
this.productionTypes.clear();
this.productionTypes.addAll(productionTypes);
}
/**
* Gets the production types available at the current difficulty
* level.
*
* @param unattended Whether the production is unattended.
* @return A list of {@code ProductionType}s.
*/
public List<ProductionType> getAvailableProductionTypes(boolean unattended) {
return getAvailableProductionTypes(unattended,
getSpecification().getString(GameOptions.TILE_PRODUCTION));
}
/**
* Gets the production types available for the given combination
* of colony center tile and production level. If the production
* level is null, all production levels will be returned.
*
* Public for the test suite.
*
* @param unattended Whether the production is unattended.
* @param level The production level.
* @return A list of {@code ProductionType}s.
*/
public List<ProductionType> getAvailableProductionTypes(boolean unattended,
String level) {
List<ProductionType> good = new ArrayList<>(),
better = new ArrayList<>();
for (ProductionType productionType : transform(productionTypes,
matchKey(unattended, ProductionType::getUnattended))) {
if (productionType.appliesExactly(level)) {
better.add(productionType);
} else if (productionType.appliesTo(level)) {
good.add(productionType);
}
}
return (!better.isEmpty()) ? better : good;
}
// Utilities
/**
* Can a tile of this type produce a given goods type?
*
* @param goodsType The {@code GoodsType} to produce.
* @param unitType An optional {@code UnitType} that is to do
* the work, if null the unattended production is considered.
* @return True if this tile type produces the goods.
*/
public boolean canProduce(GoodsType goodsType, UnitType unitType) {
return goodsType != null
&& ProductionType.canProduce(goodsType,
getAvailableProductionTypes(unitType == null));
}
/**
* Get the amount of goods of given goods type the given unit type
* could produce on a tile of this tile type.
*
* @param goodsType The {@code GoodsType} to produce.
* @param unitType An optional {@code UnitType} that is to do
* the work, if null the unattended production is considered.
* @return The amount of goods produced.
*/
public int getPotentialProduction(GoodsType goodsType,
UnitType unitType) {
if (goodsType == null) return 0;
int amount = getBaseProduction(null, goodsType, unitType);
if (unitType != null) {
amount = (int) unitType.apply(amount, null, goodsType.getId(), unitType);
} else {
/*
* XXX: The feature container is always null for TileType. What was
* the desired behaviour here?
*/
amount = (int)apply(amount, null, goodsType.getId(), unitType);
}
return (amount < 0) ? 0 : amount;
}
/**
* Get all possible goods produced at a tile of this type.
*
* Used by static tile type displays that just list unattended
* production values. Planning and production routines should use
* {@link #getPotentialProduction(GoodsType, UnitType)}
*
* @param unattended Select unattended production.
* @return A stream of produced {@code AbstractGoods}.
*/
public Stream<AbstractGoods> getPossibleProduction(boolean unattended) {
return flatten(getAvailableProductionTypes(unattended),
ProductionType::getOutputs);
}
// Override FreeColObject
/**
* {@inheritDoc}
*/
@Override
public <T extends FreeColObject> boolean copyIn(T other) {
TileType o = copyInCast(other, TileType.class);
if (o == null || !super.copyIn(o)) return false;
this.forest = o.isForested();
this.water = o.isWater();
this.canSettle = o.canSettle();
this.connected = o.isHighSeasConnected();
this.elevation = o.isElevation();
this.basicMoveCost = o.getBasicMoveCost();
this.basicWorkTurns = o.getBasicWorkTurns();
for (int i = 0; i < this.humidity.length; i++) {
this.setHumidity(i, o.getHumidity(i));
}
for (int i = 0; i < this.temperature.length; i++) {
this.setTemperature(i, o.getTemperature(i));
}
for (int i = 0; i < this.altitude.length; i++) {
this.setAltitude(i, o.getAltitude(i));
}
this.setResourceTypes(o.getResourceTypes());
this.setDisasters(o.getDisasters());
this.setProductionTypes(o.getProductionTypes());
return true;
}
// Serialization
private static final String ALTITUDE_MIN_TAG = "altitude-minimum";
private static final String ALTITUDE_MAX_TAG = "altitude-maximum";
private static final String BASIC_MOVE_COST_TAG = "basic-move-cost";
private static final String BASIC_WORK_TURNS_TAG = "basic-work-turns";
private static final String CAN_SETTLE_TAG = "can-settle";
private static final String DISASTER_TAG = "disaster";
private static final String GEN_TAG = "gen";
private static final String GOODS_TYPE_TAG = "goods-type";
private static final String HUMIDITY_MIN_TAG = "humidity-minimum";
private static final String HUMIDITY_MAX_TAG = "humidity-maximum";
private static final String IS_CONNECTED_TAG = "is-connected";
private static final String IS_ELEVATION_TAG = "is-elevation";
private static final String IS_FOREST_TAG = "is-forest";
private static final String IS_WATER_TAG = "is-water";
private static final String PROBABILITY_TAG = "probability";
private static final String PRODUCTION_TAG = "production";
private static final String RESOURCE_TAG = "resource";
private static final String TEMPERATURE_MIN_TAG = "temperature-minimum";
private static final String TEMPERATURE_MAX_TAG = "temperature-maximum";
private static final String TYPE_TAG = "type";
// @compat 0.11.x
private static final String PRIMARY_PRODUCTION_TAG = "primary-production";
private static final String SECONDARY_PRODUCTION_TAG = "secondary-production";
// end @compat 0.11.x
// @compat 0.11.3
private static final String OLD_ALTITUDE_MIN_TAG = "altitudeMin";
private static final String OLD_ALTITUDE_MAX_TAG = "altitudeMax";
private static final String OLD_HUMIDITY_MIN_TAG = "humidityMin";
private static final String OLD_HUMIDITY_MAX_TAG = "humidityMax";
private static final String OLD_TEMPERATURE_MIN_TAG = "temperatureMin";
private static final String OLD_TEMPERATURE_MAX_TAG = "temperatureMax";
// end @compat 0.11.3
/**
* {@inheritDoc}
*/
@Override
protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException {
super.writeAttributes(xw);
xw.writeAttribute(BASIC_MOVE_COST_TAG, this.basicMoveCost);
xw.writeAttribute(BASIC_WORK_TURNS_TAG, this.basicWorkTurns);
xw.writeAttribute(IS_FOREST_TAG, this.forest);
xw.writeAttribute(IS_WATER_TAG, this.water);
xw.writeAttribute(IS_ELEVATION_TAG, this.elevation);
xw.writeAttribute(IS_CONNECTED_TAG, this.connected);
xw.writeAttribute(CAN_SETTLE_TAG, this.canSettle);
}
/**
* {@inheritDoc}
*/
@Override
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
super.writeChildren(xw);
xw.writeStartElement(GEN_TAG);
xw.writeAttribute(HUMIDITY_MIN_TAG, this.humidity[0]);
xw.writeAttribute(HUMIDITY_MAX_TAG, this.humidity[1]);
xw.writeAttribute(TEMPERATURE_MIN_TAG, this.temperature[0]);
xw.writeAttribute(TEMPERATURE_MAX_TAG, this.temperature[1]);
xw.writeAttribute(ALTITUDE_MIN_TAG, this.altitude[0]);
xw.writeAttribute(ALTITUDE_MAX_TAG, this.altitude[1]);
xw.writeEndElement();
for (ProductionType productionType : this.productionTypes) {
productionType.toXML(xw);
}
if (this.resourceTypes != null) {
for (RandomChoice<ResourceType> choice : this.resourceTypes) {
xw.writeStartElement(RESOURCE_TAG);
xw.writeAttribute(TYPE_TAG, choice.getObject());
xw.writeAttribute(PROBABILITY_TAG, choice.getProbability());
xw.writeEndElement();
}
}
for (RandomChoice<Disaster> choice : iterable(getDisasterChoices())) {
xw.writeStartElement(DISASTER_TAG);
xw.writeAttribute(ID_ATTRIBUTE_TAG, choice.getObject());
xw.writeAttribute(PROBABILITY_TAG, choice.getProbability());
xw.writeEndElement();
}
}
/**
* {@inheritDoc}
*/
@Override
protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
super.readAttributes(xr);
this.basicMoveCost = xr.getAttribute(BASIC_MOVE_COST_TAG, 1);
this.basicWorkTurns = xr.getAttribute(BASIC_WORK_TURNS_TAG, 1);
this.forest = xr.getAttribute(IS_FOREST_TAG, false);
this.water = xr.getAttribute(IS_WATER_TAG, false);
this.elevation = xr.getAttribute(IS_ELEVATION_TAG, false);
this.canSettle = xr.getAttribute(CAN_SETTLE_TAG, !water);
this.connected = xr.getAttribute(IS_CONNECTED_TAG, false);
}
/**
* {@inheritDoc}
*/
@Override
protected void clearContainers(FreeColXMLReader xr) throws XMLStreamException {
super.clearContainers(xr);
this.disasters = null;
this.resourceTypes = null;
this.productionTypes.clear();
}
/**
* {@inheritDoc}
*/
@Override
protected void readChildren(FreeColXMLReader xr) throws XMLStreamException {
super.readChildren(xr);
}
/**
* {@inheritDoc}
*/
@Override
protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
final Specification spec = getSpecification();
final String tag = xr.getLocalName();
if (DISASTER_TAG.equals(tag)) {
Disaster d = xr.getType(spec, ID_ATTRIBUTE_TAG,
Disaster.class, (Disaster)null);
if (d != null) {
addDisaster(d, xr.getAttribute(PROBABILITY_TAG, 100));
}
xr.closeTag(DISASTER_TAG);
} else if (GEN_TAG.equals(tag)) {
this.humidity[0] = xr.getAttribute(HUMIDITY_MIN_TAG, 0);
this.humidity[1] = xr.getAttribute(HUMIDITY_MAX_TAG, 100);
this.temperature[0] = xr.getAttribute(TEMPERATURE_MIN_TAG, -20);
this.temperature[1] = xr.getAttribute(TEMPERATURE_MAX_TAG, 40);
this.altitude[0] = xr.getAttribute(ALTITUDE_MIN_TAG, 0);
this.altitude[1] = xr.getAttribute(ALTITUDE_MAX_TAG, 0);
// @compat 0.11.3
if (xr.hasAttribute(OLD_HUMIDITY_MIN_TAG)) {
this.humidity[0] = xr.getAttribute(OLD_HUMIDITY_MIN_TAG, 0);
}
if (xr.hasAttribute(OLD_HUMIDITY_MAX_TAG)) {
this.humidity[1] = xr.getAttribute(OLD_HUMIDITY_MAX_TAG, 100);
}
if (xr.hasAttribute(OLD_TEMPERATURE_MIN_TAG)) {
this.temperature[0] = xr.getAttribute(OLD_TEMPERATURE_MIN_TAG, -20);
}
if (xr.hasAttribute(OLD_TEMPERATURE_MAX_TAG)) {
this.temperature[1] = xr.getAttribute(OLD_TEMPERATURE_MAX_TAG, 40);
}
if (xr.hasAttribute(OLD_ALTITUDE_MIN_TAG)) {
this.altitude[0] = xr.getAttribute(OLD_ALTITUDE_MIN_TAG, 0);
}
if (xr.hasAttribute(OLD_ALTITUDE_MAX_TAG)) {
this.altitude[1] = xr.getAttribute(OLD_ALTITUDE_MAX_TAG, 0);
}
// end @compat 0.11.3
xr.closeTag(GEN_TAG);
} else if (PRODUCTION_TAG.equals(tag)
&& xr.getAttribute(DELETE_TAG, false)) {
this.productionTypes.clear();
xr.closeTag(PRODUCTION_TAG);
} else if (PRODUCTION_TAG.equals(tag)) {
this.productionTypes.add(new ProductionType(xr, spec));
} else if (RESOURCE_TAG.equals(tag)) {
addResourceType(xr.getType(spec, TYPE_TAG, ResourceType.class,
(ResourceType)null),
xr.getAttribute(PROBABILITY_TAG, 100));
xr.closeTag(RESOURCE_TAG);
// @compat 0.11.x
// Primary and secondary production was dropped at 0.11.0, but
// some saved games slipped through.
} else if (PRIMARY_PRODUCTION_TAG.equals(tag)
|| SECONDARY_PRODUCTION_TAG.equals(tag)) {
GoodsType type = xr.getType(spec, GOODS_TYPE_TAG,
GoodsType.class, (GoodsType)null);
int amount = xr.getAttribute(VALUE_TAG, 0);
ProductionType pt = new ProductionType(null, type, amount);
pt.setUnattended(true);
this.productionTypes.add(pt);
xr.closeTag(tag);
// @end compat 0.11.x
} else {
super.readChild(xr);
}
}
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
}