/**
* 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.server.model;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.freecol.client.gui.i18n.Messages;
import net.sf.freecol.common.model.Ability;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.AbstractUnit;
import net.sf.freecol.common.model.Building;
import net.sf.freecol.common.model.BuildingType;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.CombatModel;
import net.sf.freecol.common.model.CombatModel.CombatResult;
import net.sf.freecol.common.model.Disaster;
import net.sf.freecol.common.model.Effect;
import net.sf.freecol.common.model.EquipmentType;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.Europe.MigrationType;
import net.sf.freecol.common.model.Event;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.FoundingFather.FoundingFatherType;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GameOptions;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsContainer;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.HistoryEvent;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Monarch;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.StringTemplate;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.model.WorkLocation;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.LootCargoMessage;
import net.sf.freecol.common.networking.MonarchActionMessage;
import net.sf.freecol.common.util.RandomChoice;
import net.sf.freecol.common.util.Utils;
import net.sf.freecol.server.control.ChangeSet;
import net.sf.freecol.server.control.ChangeSet.ChangePriority;
import net.sf.freecol.server.control.ChangeSet.See;
/**
* A <code>Player</code> with additional (server specific) information.
*
* That is: pointers to this player's
* {@link Connection} and {@link Socket}
*/
public class ServerPlayer extends Player implements ServerModelObject {
private static final Logger logger = Logger.getLogger(ServerPlayer.class.getName());
// TODO: move to options or spec?
public static final int ALARM_RADIUS = 2;
public static final int ALARM_TILE_IN_USE = 2;
public static final int ALARM_MISSIONARY_PRESENT = -10;
// checkForDeath results
public static final int IS_DEAD = -1;
public static final int IS_ALIVE = 0;
public static final int AUTORECRUIT = 1;
// How far to search for a colony to add an Indian convert to.
public static final int MAX_CONVERT_DISTANCE = 10;
/** The network socket to the player's client. */
private Socket socket;
/** The connection for this player. */
private Connection connection;
private boolean connected = false;
/** Remaining emigrants to select due to a fountain of youth */
private int remainingEmigrants = 0;
/** Players with respect to which stance has changed. */
private List<ServerPlayer> stanceDirty = new ArrayList<ServerPlayer>();
/**
* Trivial constructor required for all ServerModelObjects.
*/
public ServerPlayer(Game game, String id) {
super(game, id);
}
/**
* Creates a new ServerPlayer.
*
* @param game The <code>Game</code> this object belongs to.
* @param name The player name.
* @param admin Whether the player is the game administrator or not.
* @param nation The nation of the <code>Player</code>.
* @param socket The socket to the player's client.
* @param connection The <code>Connection</code> for the socket.
*/
public ServerPlayer(Game game, String name, boolean admin, Nation nation,
Socket socket, Connection connection) {
super(game);
this.name = name;
this.admin = admin;
europe = null;
if (nation != null && nation.getType() != null) {
this.nationType = nation.getType();
this.nationID = nation.getId();
try {
addFeatures(nationType);
} catch (Throwable error) {
error.printStackTrace();
}
if (nationType.isEuropean()) {
/*
* Setting the amount of gold to
* "getGameOptions().getInteger(GameOptions.STARTING_MONEY)"
*
* just before starting the game. See
* "net.sf.freecol.server.control.PreGameController".
*/
this.playerType = (nationType.isREF()) ? PlayerType.ROYAL
: PlayerType.COLONIAL;
europe = new ServerEurope(game, this);
initializeHighSeas();
if (this.playerType == PlayerType.COLONIAL) {
monarch = new Monarch(game, this, nation.getRulerNameKey());
}
gold = 0;
} else { // indians
this.playerType = PlayerType.NATIVE;
gold = Player.GOLD_NOT_ACCOUNTED;
}
} else {
// virtual "enemy privateer" player
// or undead ?
this.nationID = Nation.UNKNOWN_NATION_ID;
this.playerType = PlayerType.COLONIAL;
gold = 0;
}
market = new Market(getGame(), this);
immigration = 0;
liberty = 0;
currentFather = null;
//call of super() will lead to this object being registered with AIMain
//before playerType has been set. AIMain will fall back to use of
//standard AIPlayer in this case. Set object again to fix this.
//Possible TODO: Is there a better way to do this?
final String curId = getId();
game.removeFreeColGameObject(curId);
game.setFreeColGameObject(curId, this);
this.socket = socket;
this.connection = connection;
connected = connection != null;
resetExploredTiles(getGame().getMap());
invalidateCanSeeTiles();
}
/**
* Checks if this player is currently connected to the server.
* @return <i>true</i> if this player is currently connected to the server
* and <code>false</code> otherwise.
*/
public boolean isConnected() {
return connected;
}
/**
* Sets the "connected"-status of this player.
*
* @param connected Should be <i>true</i> if this player is currently
* connected to the server and <code>false</code> otherwise.
* @see #isConnected
*/
public void setConnected(boolean connected) {
this.connected = connected;
}
/**
* Gets the socket of this player.
* @return The <code>Socket</code>.
*/
public Socket getSocket() {
return socket;
}
/**
* Gets the connection of this player.
*
* @return The <code>Connection</code>.
*/
public Connection getConnection() {
return connection;
}
/**
* Sets the connection of this player.
*
* @param connection The <code>Connection</code>.
*/
public void setConnection(Connection connection) {
this.connection = connection;
connected = (connection != null);
}
/**
* Performs initial randomizations for this player.
*
* @param random A pseudo-random number source.
*/
public void startGame(Random random) {
Specification spec = getGame().getSpecification();
if (isEuropean() && !isREF()) {
modifyGold(spec.getIntegerOption(GameOptions.STARTING_MONEY)
.getValue());
((ServerEurope) getEurope()).initializeMigration(random);
getMarket().randomizeInitialPrice(random);
}
}
/**
* Checks if this player has died.
*
* @return Negative if the player is dead, zero if they are ok,
* positive non-zero if the server should auto-recruit
* colonist units to keep this player alive.
*/
public int checkForDeath() {
final Specification spec = getGame().getSpecification();
/*
* Die if: (isNative && (no colonies or units))
* || ((rebel or independent) && !(has coastal colony))
* || (isREF && !(rebel nation left) && (all units in Europe))
* || ((no units in New World)
* && ((year > 1600) || (cannot get a unit from Europe)))
*/
switch (getPlayerType()) {
case NATIVE: // All natives units are viable
return (getUnits().isEmpty()) ? IS_DEAD : IS_ALIVE;
case COLONIAL: // Handle the hard case below
if (isUnknownEnemy()) return IS_ALIVE;
break;
case REBEL: case INDEPENDENT:
// Post-declaration European player needs a coastal colony
// and can not hope for resupply from Europe.
for (Colony colony : getColonies()) {
if (colony.isConnectedPort()) return IS_ALIVE;
}
return IS_DEAD;
case ROYAL:
return (getRebels().isEmpty()) ? IS_DEAD : IS_ALIVE;
case UNDEAD:
return (getUnits().isEmpty()) ? IS_DEAD : IS_ALIVE;
default:
throw new IllegalStateException("Bogus player type");
}
// Quick check for a colony. Do not log, this is the common case.
if (!getColonies().isEmpty()) return IS_ALIVE;
// Traverse player units, look for valid carriers, colonists,
// carriers with units, carriers with goods.
boolean hasCarrier = false, hasColonist = false, hasEmbarked = false,
hasGoods = false;
for (Unit unit : getUnits()) {
if (unit.isCarrier()) {
if (unit.hasGoodsCargo()) hasGoods = true;
hasCarrier = true;
continue;
}
// Must be able to found new colony or capture units
if (!unit.isColonist() && !unit.isOffensiveUnit()) continue;
hasColonist = true;
// Verify if unit is in new world, or on a carrier in new world
Unit carrier;
if ((carrier = unit.getCarrier()) != null) {
if (carrier.getTile() != null) {
logger.info(getName() + " alive, unit (embarked) on map.");
return IS_ALIVE;
}
hasEmbarked = true;
}
if (unit.getTile() != null) {
logger.info(getName() + " alive, unit on map.");
return IS_ALIVE;
}
}
// The player does not have any valid units or settlements on the map.
int mandatory = spec.getInteger("model.option.mandatoryColonyYear");
if (getGame().getTurn().getYear() >= mandatory) {
// After the season cutover year there must be a presence
// in the New World.
logger.info(getName() + " dead, no presence >= " + mandatory);
return IS_DEAD;
}
// No problems, unit available on carrier but off map, or goods
// available to be sold.
if (hasEmbarked) {
logger.info(getName() + " alive, has embarked unit.");
return IS_ALIVE;
} else if (hasGoods) {
logger.info(getName() + " alive, has cargo.");
return IS_ALIVE;
}
// It is necessary to still have a carrier.
final Europe europe = getEurope();
int goldNeeded = 0;
if (!hasCarrier) {
int price = Integer.MAX_VALUE;
if (europe != null) {
for (UnitType type
: spec.getUnitTypesWithAbility(Ability.NAVAL_UNIT)) {
int p = europe.getUnitPrice(type);
if (p != UNDEFINED && p < price) price = p;
}
}
if (price == Integer.MAX_VALUE || !checkGold(price)) {
logger.info(getName() + " dead, can not buy carrier.");
return IS_DEAD;
}
goldNeeded += price;
}
// A colonist is required.
if (hasColonist) {
logger.info(getName() + " alive, has waiting colonist.");
return IS_ALIVE;
} else if (europe == null) {
logger.info(getName() + " dead, can not recruit.");
return IS_DEAD;
}
UnitType unitType = null;
int price = europe.getRecruitPrice();
for (UnitType type : spec
.getUnitTypesWithAbility("model.ability.foundColony")) {
int p = europe.getUnitPrice(type);
if (p != UNDEFINED && p < price) price = p;
}
goldNeeded += price;
if (checkGold(goldNeeded)) {
logger.info(getName() + " alive, can buy colonist.");
return IS_ALIVE;
}
// Col1 auto-recruits a unit in Europe if you run out before
// the cutover year.
logger.info(getName() + " survives by autorecruit.");
return AUTORECRUIT;
}
/**
* Check if a REF player has been defeated and should surrender.
*
* @return True if this REF player has been defeated.
*/
public boolean checkForREFDefeat() {
if (!isREF()) {
throw new IllegalStateException("Checking for REF player defeat when player not REF.");
}
// No one to fight? Either the rebels are dead, or the REF
// was already defeated and the rebels are independent.
// Either way, it does not need to surrender.
if (getRebels().isEmpty()) return false;
// Not defeated if there are settlements.
if (!getSettlements().isEmpty()) return false;
// Not defeated if there is a non-zero navy and enough land units.
final int landREFUnitsRequired = 7; // TODO: magic number
final CombatModel cm = getGame().getCombatModel();
boolean naval = false;
int land = 0;
int power = 0;
for (Unit u : getUnits()) {
if (u.isNaval()) naval = true; else {
if (u.hasAbility("model.ability.refUnit")) {
land++;
power += cm.getOffencePower(u, null);
}
}
}
if (naval && land >= landREFUnitsRequired) return false;
// Still not defeated as long as military strength is greater
// than the rebels.
int rebelPower = 0;
for (Player rebel : getRebels()) {
for (Unit r : rebel.getUnits()) {
if (!r.isNaval()) rebelPower += cm.getOffencePower(r, null);
}
}
if (power > rebelPower) return false;
// REF is defeated
return true;
}
/**
* Kills the missionary in a settlement.
*
* @param settlement The <code>IndianSettlement</code> to kill the
* missionary from.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csKillMissionary(IndianSettlement settlement, ChangeSet cs) {
Unit missionary = settlement.getMissionary();
settlement.changeMissionary(null);
// Inform the enemy of loss of mission
cs.add(See.only(this), settlement);
cs.addDispose(See.perhaps().always(this),
settlement.getTile(), missionary);
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"indianSettlement.mission.denounced", settlement)
.addStringTemplate("%settlement%", settlement.getLocationNameFor(this)));
}
/**
* Kill off a player and clear out its remains.
*
* @param cs A <code>ChangeSet</code> to update.
*/
public void csKill(ChangeSet cs) {
setDead(true);
cs.addPartial(See.all(), this, "dead");
cs.addDead(this);
// Clean up missions and remove tension/alarm/stance.
for (Player other : getGame().getPlayers()) {
if (other == this) continue;
if (isEuropean() && other.isIndian()) {
for (IndianSettlement s : other.getIndianSettlements()) {
Unit unit = s.getMissionary();
if (unit != null
&& ((ServerPlayer) unit.getOwner()) == this) {
csKillMissionary(s, cs);
}
s.removeAlarm(this);
}
other.removeTension(this);
}
other.setStance(this, null);
}
// Remove settlements. Update formerly owned tiles.
List<Settlement> settlements = getSettlements();
while (!settlements.isEmpty()) {
Settlement settlement = settlements.remove(0);
cs.addDispose(See.perhaps().always(this),
settlement.getTile(), settlement);
}
// Clean up remaining tile ownerships
for (Tile tile : getGame().getMap().getAllTiles()) {
if (tile.getOwner() == this) {
tile.changeOwnership(null, null);
cs.add(See.perhaps().always(this), tile);
}
}
// Remove units
List<Unit> units = getUnits();
while (!units.isEmpty()) {
Unit unit = units.remove(0);
if (unit.getLocation() instanceof Tile) {
cs.add(See.perhaps().always(this), unit.getTile());
}
cs.addDispose(See.perhaps().always(this),
unit.getLocation(), unit);
}
}
/**
* Withdraw a player from the new world.
*
* @param cs A <code>ChangeSet</code> to update.
*/
public void csWithdraw(ChangeSet cs) {
cs.addMessage(See.all(),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
((isEuropean() && getPlayerType() == PlayerType.COLONIAL)
? "model.diplomacy.dead.european"
: "model.diplomacy.dead.native"),
this)
.addStringTemplate("%nation%", getNationName()));
Game game = getGame();
cs.addGlobalHistory(game,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.NATION_DESTROYED)
.addStringTemplate("%nation%", getNationName()));
csKill(cs);
}
public int getRemainingEmigrants() {
return remainingEmigrants;
}
public void setRemainingEmigrants(int emigrants) {
remainingEmigrants = emigrants;
}
/**
* Checks whether the current founding father has been recruited.
*
* @return The new founding father, or null if none available or ready.
*/
public FoundingFather checkFoundingFather() {
FoundingFather father = null;
if (currentFather != null) {
int extraLiberty = getRemainingFoundingFatherCost();
if (extraLiberty <= 0) {
boolean overflow = getSpecification()
.getBoolean(GameOptions.SAVE_PRODUCTION_OVERFLOW);
setLiberty((overflow) ? -extraLiberty : 0);
father = currentFather;
currentFather = null;
}
}
return father;
}
/**
* Checks whether to start recruiting a founding father.
*
* @return True if a new father should be chosen.
*/
public boolean canRecruitFoundingFather() {
Specification spec = getGame().getSpecification();
switch (getPlayerType()) {
case COLONIAL:
break;
case REBEL: case INDEPENDENT:
if (!spec.getBoolean("model.option.continueFoundingFatherRecruitment")) return false;
break;
default:
return false;
}
return canHaveFoundingFathers() && currentFather == null
&& !getSettlements().isEmpty()
&& getFatherCount() < spec.getFoundingFathers().size();
}
/**
* Build a list of random FoundingFathers, one per type.
* Do not include any the player has or are not available.
*
* @param random A pseudo-random number source.
* @return A list of FoundingFathers.
*/
public List<FoundingFather> getRandomFoundingFathers(Random random) {
// Build weighted random choice for each father type
Specification spec = getGame().getSpecification();
int age = getGame().getTurn().getAge();
EnumMap<FoundingFatherType, List<RandomChoice<FoundingFather>>> choices
= new EnumMap<FoundingFatherType,
List<RandomChoice<FoundingFather>>>(FoundingFatherType.class);
for (FoundingFather father : spec.getFoundingFathers()) {
if (!hasFather(father) && father.isAvailableTo(this)) {
FoundingFatherType type = father.getType();
List<RandomChoice<FoundingFather>> rc = choices.get(type);
if (rc == null) {
rc = new ArrayList<RandomChoice<FoundingFather>>();
}
int weight = father.getWeight(age);
rc.add(new RandomChoice<FoundingFather>(father, weight));
choices.put(father.getType(), rc);
}
}
// Select one from each father type
List<FoundingFather> randomFathers = new ArrayList<FoundingFather>();
String logMessage = "Random fathers";
for (FoundingFatherType type : FoundingFatherType.values()) {
List<RandomChoice<FoundingFather>> rc = choices.get(type);
if (rc != null) {
FoundingFather f = RandomChoice.getWeightedRandom(logger,
"Choose founding father", random, rc);
if (f != null) {
randomFathers.add(f);
logMessage += ":" + f.getNameKey();
}
}
}
logger.info(logMessage);
return randomFathers;
}
/**
* Generate a weighted list of unit types recruitable by this player.
*
* @return A weighted list of recruitable unit types.
*/
public List<RandomChoice<UnitType>> generateRecruitablesList() {
ArrayList<RandomChoice<UnitType>> recruitables
= new ArrayList<RandomChoice<UnitType>>();
for (UnitType unitType : getSpecification().getUnitTypeList()) {
if (unitType.isRecruitable()
&& hasAbility("model.ability.canRecruitUnit", unitType)) {
recruitables.add(new RandomChoice<UnitType>(unitType,
unitType.getRecruitProbability()));
}
}
return recruitables;
}
/**
* Add a HistoryEvent to this player.
*
* @param event The <code>HistoryEvent</code> to add.
*/
public void addHistory(HistoryEvent event) {
history.add(event);
}
/**
* Resets this player's explored tiles. This is done by setting
* all the tiles within a {@link Unit}s line of sight visible.
* The other tiles are made unvisible.
*
* @param map The <code>Map</code> to reset the explored tiles on.
* @see #hasExplored
*/
public void resetExploredTiles(Map map) {
if (map != null) {
for (Unit unit : getUnits()) {
Tile tile = unit.getTile();
setExplored(tile);
int radius = (unit.getColony() != null)
? unit.getColony().getLineOfSight()
: unit.getLineOfSight();
for (Tile t : tile.getSurroundingTiles(radius)) {
setExplored(t);
}
}
}
}
/**
* Checks if this <code>Player</code> has explored the given
* <code>Tile</code>.
*
* @param tile The <code>Tile</code>.
* @return <i>true</i> if the <code>Tile</code> has been explored and
* <i>false</i> otherwise.
*/
public boolean hasExplored(Tile tile) {
return tile.isExploredBy(this);
}
/**
* Sets the given tile to be explored by this player and updates
* the player's information about the tile.
*/
public void setExplored(Tile tile) {
tile.setExploredBy(this, true);
}
/**
* Makes the entire map visible.
* Debug mode helper.
*
* @param reveal If true, reveal the map, if false, hide it.
* @return A list of tiles whose visibility changed.
*/
public List<Tile> exploreMap(boolean reveal) {
List<Tile> result = new ArrayList<Tile>();
for (Tile tile : getGame().getMap().getAllTiles()) {
if (tile.isExploredBy(this) != reveal) {
tile.setExploredBy(this, reveal);
result.add(tile);
}
}
resetExploredTiles(getGame().getMap());
invalidateCanSeeTiles();
return result;
}
/**
* Sets the tiles within the given <code>Unit</code>'s line of
* sight to be explored by this player.
*
* @param unit The <code>Unit</code>.
* @see #setExplored(Tile)
* @see #hasExplored
*/
public void setExplored(Unit unit) {
if (getGame() == null || getGame().getMap() == null || unit == null
|| unit.getLocation() == null || unit.getTile() == null) {
return;
}
Tile tile = unit.getTile();
setExplored(tile);
for (Tile t : tile.getSurroundingTiles(unit.getLineOfSight())) {
setExplored(t);
}
invalidateCanSeeTiles();
}
/**
* Create units from a list of abstract units. Only used by
* Europeans at present.
*
* @param abstractUnits The list of <code>AbstractUnit</code>s to create.
* @param location The location where the units will be created.
* @return A list of units created.
*/
public List<Unit> createUnits(List<AbstractUnit> abstractUnits, Location location) {
Game game = getGame();
List<Unit> units = new ArrayList<Unit>();
if (location == null) return units;
Specification spec = game.getSpecification();
for (AbstractUnit au : abstractUnits) {
for (int i = 0; i < au.getNumber(); i++) {
units.add(new ServerUnit(game, location, this,
au.getUnitType(spec), au.getEquipment(spec)));
}
}
return units;
}
/**
* Embark the land units. For all land units, find a naval unit
* to carry it. Fill greedily, so as if there is excess naval
* capacity then the naval units at the end of the list will tend
* to be empty or very lightly filled, allowing them to defend the
* whole fleet at full strength. Returns a list of units that
* could not be placed on ships.
*
* @param landUnits the land units to put on ships
* @param navalUnits the ships to put land units on
* @param random a PRNG
* @return a list of units left over
*/
public List<Unit> loadShips(List<Unit> landUnits, List<Unit> navalUnits,
Random random) {
List<Unit> leftOver = new ArrayList<Unit>();
Collections.shuffle(navalUnits, random);
Collections.shuffle(landUnits, random);
landUnit: for (Unit unit : landUnits) {
for (Unit carrier : navalUnits) {
if (carrier.canAdd(unit)) {
unit.setLocation(carrier);
logger.finest("Loading " + unit
+ " onto carrier " + carrier);
continue landUnit;
}
}
leftOver.add(unit);
}
return leftOver;
}
/**
* Calculates the price of a group of mercenaries for this player.
*
* @param mercenaries A list of mercenaries to price.
* @return The price.
*/
public int priceMercenaries(List<AbstractUnit> mercenaries) {
int mercPrice = 0;
for (AbstractUnit au : mercenaries) {
mercPrice += getPrice(au);
}
if (!checkGold(mercPrice)) mercPrice = getGold();
return mercPrice;
}
/**
* Propagate an European market change to the other European markets.
*
* @param type The type of goods that was traded.
* @param amount The amount of goods that was traded.
* @param random A <code>Random</code> number source.
*/
private void propagateToEuropeanMarkets(GoodsType type, int amount,
Random random) {
if (!type.isStorable()) return;
// Propagate 5-30% of the original change.
final int lowerBound = 5; // TODO: make into game option?
final int upperBound = 30;// TODO: make into game option?
amount *= Utils.randomInt(logger, "Propagate goods", random,
upperBound - lowerBound + 1) + lowerBound;
amount /= 100;
if (amount == 0) return;
// Do not need to update the clients here, these changes happen
// while it is not their turn.
for (Player other : getGame().getLiveEuropeanPlayers()) {
Market market;
if ((ServerPlayer) other != this
&& (market = other.getMarket()) != null) {
market.addGoodsToMarket(type, amount);
}
}
}
/**
* Flush any market price changes for a specified goods type.
*
* @param type The <code>GoodsType</code> to check.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csFlushMarket(GoodsType type, ChangeSet cs) {
Market market = getMarket();
if (market.hasPriceChanged(type)) {
// This type of goods has changed price, so we will update
// the market and send a message as well.
cs.addMessage(See.only(this),
market.makePriceChangeMessage(type));
market.flushPriceChange(type);
cs.add(See.only(this), market.getMarketData(type));
}
}
/**
* Buy goods in Europe.
*
* @param container The <code>GoodsContainer</code> to carry the goods.
* @param type The <code>GoodsType</code> to buy.
* @param amount The amount of goods to buy.
* @param random A <code>Random</code> number source.
* @throws IllegalStateException If the <code>player</code> cannot afford
* to buy the goods.
*/
public void buy(GoodsContainer container, GoodsType type, int amount,
Random random)
throws IllegalStateException {
logger.finest(getName() + " buys " + amount + " " + type);
Market market = getMarket();
int price = market.getBidPrice(type, amount);
if (!checkGold(price)) {
throw new IllegalStateException("Player " + getName()
+ " tried to buy " + Integer.toString(amount)
+ " " + type.toString()
+ " for " + Integer.toString(price)
+ " but has " + Integer.toString(getGold()) + " gold.");
}
modifyGold(-price);
market.modifySales(type, -amount);
market.modifyIncomeBeforeTaxes(type, -price);
market.modifyIncomeAfterTaxes(type, -price);
int marketAmount = -(int)applyModifier((float)amount,
"model.modifier.tradeBonus",
type, getGame().getTurn());
market.addGoodsToMarket(type, marketAmount);
propagateToEuropeanMarkets(type, marketAmount, random);
container.addGoods(type, amount);
}
/**
* Sell goods in Europe.
*
* @param container An optional <code>GoodsContainer</code>
* carrying the goods.
* @param type The <code>GoodsType</code> to sell.
* @param amount The amount of goods to sell.
* @param random A <code>Random</code> number source.
*/
public void sell(GoodsContainer container, GoodsType type, int amount,
Random random) {
logger.finest(getName() + " sells " + amount + " " + type);
Market market = getMarket();
int tax = getTax();
int incomeBeforeTaxes = market.getSalePrice(type, amount);
int incomeAfterTaxes = ((100 - tax) * incomeBeforeTaxes) / 100;
modifyGold(incomeAfterTaxes);
market.modifySales(type, amount);
market.modifyIncomeBeforeTaxes(type, incomeBeforeTaxes);
market.modifyIncomeAfterTaxes(type, incomeAfterTaxes);
int marketAmount = (int)applyModifier((float)amount,
"model.modifier.tradeBonus",
type, getGame().getTurn());
market.addGoodsToMarket(type, marketAmount);
propagateToEuropeanMarkets(type, marketAmount, random);
if (container != null) container.addGoods(type, -amount);
}
/**
* Adds a player to the list of players for whom the stance has changed.
*
* @param other The <code>ServerPlayer</code> to add.
*/
public void addStanceChange(ServerPlayer other) {
if (!stanceDirty.contains(other)) stanceDirty.add(other);
}
/**
* Modifies stance.
*
* @param stance The new <code>Stance</code>.
* @param otherPlayer The <code>Player</code> wrt which the stance changes.
* @param symmetric If true, change the otherPlayer stance as well.
* @param cs A <code>ChangeSet</code> to update.
* @return True if there was a change in stance at all.
*/
public boolean csChangeStance(Stance stance, Player otherPlayer,
boolean symmetric, ChangeSet cs) {
ServerPlayer other = (ServerPlayer) otherPlayer;
boolean change = false;
Stance old = getStance(otherPlayer);
if (old != stance) {
int modifier = old.getTensionModifier(stance);
setStance(otherPlayer, stance);
if (modifier != 0) {
cs.add(See.only(null).perhaps(other),
modifyTension(otherPlayer, modifier));
}
logger.info("Stance modification " + getName()
+ " " + old.toString() + " -> " + stance.toString()
+ " wrt " + otherPlayer.getName());
this.addStanceChange(other);
cs.addMessage(See.only(other),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.diplomacy." + stance + ".declared", this)
.addStringTemplate("%nation%", getNationName()));
cs.addStance(See.only(this), this, stance, otherPlayer);
cs.addStance(See.only(other), this, stance, otherPlayer);
change = true;
}
if (symmetric && (old = otherPlayer.getStance(this)) != stance) {
int modifier = old.getTensionModifier(stance);
otherPlayer.setStance(this, stance);
if (modifier != 0) {
cs.add(See.only(null).perhaps(this),
otherPlayer.modifyTension(this, modifier));
}
logger.info("Stance modification " + otherPlayer.getName()
+ " " + old.toString() + " -> " + stance.toString()
+ " wrt " + getName() + " (symmetric)");
other.addStanceChange(this);
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.diplomacy." + stance + ".declared", otherPlayer)
.addStringTemplate("%nation%", otherPlayer.getNationName()));
cs.addStance(See.only(this), otherPlayer, stance, this);
cs.addStance(See.only(other), otherPlayer, stance, this);
change = true;
}
return change;
}
/**
* New turn for this player.
*
* @param random A <code>Random</code> number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csNewTurn(Random random, ChangeSet cs) {
logger.finest("ServerPlayer.csNewTurn, for " + getName());
// Settlements
List<Settlement> settlements
= new ArrayList<Settlement>(getSettlements());
int newSoL = 0;
for (Settlement settlement : settlements) {
((ServerModelObject) settlement).csNewTurn(random, cs);
newSoL += settlement.getSoL();
}
int numberOfColonies = settlements.size();
if (numberOfColonies > 0) {
newSoL = newSoL / numberOfColonies;
if (oldSoL / 10 != newSoL / 10) {
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.SONS_OF_LIBERTY,
(newSoL > oldSoL)
? "model.player.SoLIncrease"
: "model.player.SoLDecrease", this)
.addAmount("%oldSoL%", oldSoL)
.addAmount("%newSoL%", newSoL));
}
oldSoL = newSoL; // Remember SoL for check changes at next turn.
}
// Europe.
if (europe != null) {
((ServerModelObject) europe).csNewTurn(random, cs);
}
// Units.
for (Unit unit : new ArrayList<Unit>(getUnits())) {
try {
((ServerModelObject) unit).csNewTurn(random, cs);
} catch (ClassCastException e) {
logger.log(Level.SEVERE, "Not a ServerUnit: " + unit.getId(), e);
}
}
if (isEuropean()) { // Update liberty and immigration
if (checkEmigrate()
&& !hasAbility("model.ability.selectRecruit")) {
// Auto-emigrate if selection not allowed.
csEmigrate(0, MigrationType.NORMAL, random, cs);
} else {
cs.addPartial(See.only(this), this, "immigration");
}
cs.addPartial(See.only(this), this, "liberty");
if (getSpecification().getBoolean(GameOptions.ENABLE_UPKEEP)) {
csPayUpkeep(random, cs);
}
int probability = getSpecification().getInteger(GameOptions.NATURAL_DISASTERS);
if (probability > 0) {
csNaturalDisasters(random, cs, probability);
}
if (getPlayerType() == PlayerType.REBEL
&& interventionBells >= getSpecification().getInteger("model.option.interventionBells")) {
interventionBells = Integer.MIN_VALUE;
// TODO: this assumes that the entry location will
// always be a tile. This seems safe enough at the moment.
Tile entryLocation = ((Tile) getEntryLocation()).getSafeTile(this, random);
List<Unit> landUnits =
createUnits(getMonarch().getInterventionForce().getLandUnits(),
entryLocation);
List<Unit> navalUnits =
createUnits(getMonarch().getInterventionForce().getNavalUnits(),
entryLocation);
List<Unit> leftOver = loadShips(landUnits, navalUnits, random);
for (Unit unit : leftOver) {
// no use for left over units
logger.warning("Disposing of left over unit " + unit);
unit.setLocation(null);
unit.dispose();
}
cs.add(See.perhaps(), entryLocation);
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.DEFAULT,
"declareIndependence.interventionForceArrives",
this));
}
}
// Update stances
while (!stanceDirty.isEmpty()) {
ServerPlayer s = stanceDirty.remove(0);
Stance sta = getStance(s);
boolean war = sta == Stance.WAR;
if (sta == Stance.UNCONTACTED) continue;
for (Player p : getGame().getLiveEuropeanPlayers()) {
ServerPlayer sp = (ServerPlayer) p;
if (sp == this || p == s
|| !p.hasContacted(this) || !p.hasContacted(p)) continue;
if (p.hasAbility("model.ability.betterForeignAffairsReport")
|| war) {
cs.addStance(See.only(sp), this, sta, s);
cs.addMessage(See.only(sp),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.diplomacy." + sta + ".others", this)
.addStringTemplate("%attacker%", getNationName())
.addStringTemplate("%defender%", s.getNationName()));
}
}
}
}
public void csPayUpkeep(Random random, ChangeSet cs) {
Disaster bankruptcy = getSpecification().getDisaster(Disaster.BANKRUPTCY);
int upkeep = 0;
for (Settlement settlement : settlements) {
upkeep += settlement.getUpkeep();
}
if (getGold() >= upkeep) {
modifyGold(-upkeep);
if (isBankrupt()) {
setBankrupt(false);
// the only effects of a disaster that can be reversed
// are the modifiers
for (RandomChoice<Effect> effect: bankruptcy.getEffects()) {
for (Modifier modifier : effect.getObject().getModifiers()) {
cs.addFeatureChange(this, this, modifier, false);
}
}
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.GOVERNMENT_EFFICIENCY,
"model.disaster.bankruptcy.stop", this));
}
} else {
modifyGold(-getGold());
if (!isBankrupt()) {
setBankrupt(true);
csApplyDisaster(random, cs, null, bankruptcy);
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.GOVERNMENT_EFFICIENCY,
"model.disaster.bankruptcy.start", this));
}
}
cs.addPartial(See.only(this), this, "bankrupt");
cs.addPartial(See.only(this), this, "gold");
}
public void csNaturalDisasters(Random random, ChangeSet cs, int probability) {
if (Utils.randomInt(logger, "check for natural disasters", random, 100) < probability) {
int size = getNumberOfSettlements();
if (size < 1) return;
// randomly select a colony to start with, then generate
// an appropriate disaster if possible, else continue with
// the next colony
int start = Utils.randomInt(logger, "select colony", random, size);
for (int index = 0; index < size; index++) {
Colony colony = getColonies().get((start + index) % size);
List<RandomChoice<Disaster>> disasters = colony.getDisasters();
if (!disasters.isEmpty()) {
Disaster disaster = RandomChoice
.getWeightedRandom(logger, "select disaster", random, disasters);
List<ModelMessage> messages = csApplyDisaster(random, cs, colony, disaster);
if (!messages.isEmpty()) {
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.DEFAULT,
"model.disaster.strikes", this)
.addName("%colony%", colony.getName())
.addName("%disaster%", disaster));
for (ModelMessage message : messages) {
cs.addMessage(See.only(this), message);
}
return;
}
}
}
}
}
/**
* Apply the effects of the given <code>Disaster</code> to the
* given <code>Colony</code>, or the <code>Player</code> if the
* <code>Colony</code> is <code>null</code>, and return a list of
* appropriate <code>ModelMessage</code>s. Note that a disaster
* might have no effect on a particular colony. In that case, the
* returned list is empty.
*
* @param random A <code>Random</code> number source.
* @param cs A <code>ChangeSet</code> to update.
* @param colony A <code>Colony</code>, or <code>null</code>.
* @param disaster A <code>Disaster</code> value.
* @return A list of <code>ModelMessage</code>s, possibly empty.
*/
public List<ModelMessage> csApplyDisaster(Random random, ChangeSet cs, Colony colony, Disaster disaster) {
logger.finest("Applying effects of disaster " + disaster.getId());
List<Effect> effects = new ArrayList<Effect>();
switch(disaster.getNumberOfEffects()) {
case ONE:
effects.add(RandomChoice.getWeightedRandom(logger, "Get effects of disaster",
random, disaster.getEffects()));
break;
case SEVERAL:
for (RandomChoice<Effect> effect : disaster.getEffects()) {
if (Utils.randomInt(logger, "Get effects of disaster", random, 100)
< effect.getProbability()) {
effects.add(effect.getObject());
}
}
break;
case ALL:
for (RandomChoice<Effect> effect : disaster.getEffects()) {
effects.add(effect.getObject());
}
}
List<ModelMessage> messages = new ArrayList<ModelMessage>();
for (Effect effect : effects) {
if (colony == null) {
// currently, the only effects that can apply to the
// player itself are production modifiers
for (Modifier modifier : effect.getModifiers()) {
if (modifier.getDuration() > 0) {
Modifier timedModifier = Modifier
.makeTimedModifier(modifier.getId(), modifier, getGame().getTurn());
cs.addFeatureChange(this, this, timedModifier, true);
} else {
cs.addFeatureChange(this, this, modifier, true);
}
}
} else {
if (Effect.LOSS_OF_MONEY.equals(effect.getId())) {
int plunder = Math.max(1, colony.getPlunder(null, random) / 5);
modifyGold(-plunder);
cs.addPartial(See.only(this), this, "gold");
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
effect.getId(), this)
.addAmount("%amount%", plunder));
} else if (Effect.LOSS_OF_GOODS.equals(effect.getId())) {
Goods goods = Utils.getRandomMember(logger, "select goods",
colony.getLootableGoodsList(), random);
goods.setAmount(Math.min(goods.getAmount() / 2, 50));
colony.removeGoods(goods);
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
effect.getId(), this)
.addName("%goods%", goods));
} else if (Effect.LOSS_OF_UNIT.equals(effect.getId())) {
Unit unit = getUnitForEffect(colony, effect, random);
if (unit != null) {
if (colony.getUnitCount() == 1) {
csDisposeSettlement(colony, cs);
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
"model.disaster.effect.colonyDestroyed", this)
.addName("%colony%", colony));
} else {
cs.addDispose(See.perhaps().always(this),
unit.getLocation(), unit);
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
effect.getId(), this)
.add("%unit%", unit.getType().getNameKey()));
}
}
} else if (Effect.DAMAGED_UNIT.equals(effect.getId())) {
Unit unit = getUnitForEffect(colony, effect, random);
if (unit != null && unit.isNaval()) {
Location repairLocation = unit.getRepairLocation();
if (repairLocation == null) {
csSinkShip(unit, null, cs);
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
"model.disaster.effect.lossOfUnit", this)
.add("%unit%", unit.getType().getNameKey()));
} else {
csDamageShip(unit, repairLocation, cs);
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
effect.getId(), this)
.addName("%unit%", unit));
}
}
} else {
messages.add(new ModelMessage(ModelMessage.MessageType.DEFAULT,
effect.getId(), this));
for (Modifier modifier : effect.getModifiers()) {
if (modifier.getDuration() > 0) {
Modifier timedModifier = Modifier
.makeTimedModifier(modifier.getId(), modifier, getGame().getTurn());
cs.addFeatureChange(this, colony, timedModifier, true);
} else {
cs.addFeatureChange(this, colony, modifier, true);
}
}
}
}
}
return messages;
}
public Unit getUnitForEffect(Colony colony, Effect effect, Random random) {
List<Unit> units = new ArrayList<Unit>();
for (Unit unit : colony.getUnitList()) {
if (effect.appliesTo(unit.getType())) {
units.add(unit);
}
}
if (units.isEmpty()) {
return null;
} else {
return Utils.getRandomMember(logger, "select unit", units, random);
}
}
/**
* Starts a new turn for a player.
* Carefully do any random number generation outside of any
* threads that start so as to keep random number generation
* deterministic.
*
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csStartTurn(Random random, ChangeSet cs) {
Game game = getGame();
if (isEuropean()) {
csBombardEnemyShips(random, cs);
csYearlyGoodsAdjust(random, cs);
FoundingFather father = checkFoundingFather();
if (father != null) {
csAddFoundingFather(father, random, cs);
clearOfferedFathers();
}
} else if (isIndian()) {
// We do not have to worry about Player level stance
// changes driving Stance, as that is delegated to the AI.
//
// However we want to notify of individual settlements
// that change tension level, but there are complex
// interactions between settlement and player tensions.
// The simple way to do it is just to save all old tension
// levels and check if they have changed after applying
// all the changes.
List<IndianSettlement> allSettlements = getIndianSettlements();
java.util.Map<IndianSettlement,
java.util.Map<Player, Tension.Level>> oldLevels
= new HashMap<IndianSettlement,
java.util.Map<Player, Tension.Level>>();
for (IndianSettlement settlement : allSettlements) {
java.util.Map<Player, Tension.Level> oldLevel
= new HashMap<Player, Tension.Level>();
oldLevels.put(settlement, oldLevel);
for (Player enemy : game.getLiveEuropeanPlayers()) {
Tension alarm = settlement.getAlarm(enemy);
if (alarm != null) oldLevel.put(enemy, alarm.getLevel());
}
}
// Do the settlement alarms first.
for (IndianSettlement settlement : allSettlements) {
java.util.Map<Player, Integer> extra
= new HashMap<Player, Integer>();
for (Player enemy : game.getLiveEuropeanPlayers()) {
extra.put(enemy, new Integer(0));
}
// Look at the uses of tiles surrounding the settlement.
int alarmRadius = settlement.getRadius() + ALARM_RADIUS;
int alarm = 0;
for (Tile tile: settlement.getTile()
.getSurroundingTiles(alarmRadius)) {
Colony colony = tile.getColony();
if (tile.getFirstUnit() != null) { // Military units
Player enemy = tile.getFirstUnit().getOwner();
if (enemy.isEuropean()) {
alarm = extra.get(enemy);
for (Unit unit : tile.getUnitList()) {
if (unit.isOffensiveUnit() && !unit.isNaval()) {
alarm += unit.getType().getOffence();
}
}
extra.put(enemy, alarm);
}
} else if (colony != null) { // Colonies
Player enemy = colony.getOwner();
extra.put(enemy, extra.get(enemy).intValue()
+ ALARM_TILE_IN_USE
+ colony.getUnitCount());
} else if (tile.getOwningSettlement() != null) { // Control
Player enemy = tile.getOwningSettlement().getOwner();
if (enemy != null && enemy.isEuropean()) {
extra.put(enemy, extra.get(enemy).intValue()
+ ALARM_TILE_IN_USE);
}
}
}
// Missionary helps reducing alarm a bit
if (settlement.getMissionary() != null) {
Unit mission = settlement.getMissionary();
int missionAlarm = ALARM_MISSIONARY_PRESENT;
if (mission.hasAbility(Ability.EXPERT_MISSIONARY)) {
missionAlarm *= 2;
}
Player enemy = mission.getOwner();
extra.put(enemy,
extra.get(enemy).intValue() + missionAlarm);
}
// Apply modifiers, and commit the total change.
for (Entry<Player, Integer> entry : extra.entrySet()) {
Player player = entry.getKey();
int change = entry.getValue().intValue();
if (change != 0) {
change = (int)player.applyModifier((float)change,
"model.modifier.nativeAlarmModifier",
null, game.getTurn());
((ServerIndianSettlement)settlement).modifyAlarm(player,
change);
}
}
}
// Calm down a bit at the whole-tribe level.
for (Player enemy : game.getLiveEuropeanPlayers()) {
if (getTension(enemy).getValue() > 0) {
int change = -getTension(enemy).getValue()/100 - 4;
modifyTension(enemy, change);
}
}
// Now collect the settlements that changed.
for (IndianSettlement settlement : allSettlements) {
java.util.Map<Player, Tension.Level> oldLevel
= oldLevels.get(settlement);
for (Entry<Player, Tension.Level> entry : oldLevel.entrySet()) {
Player enemy = entry.getKey();
Tension.Level newLevel
= settlement.getAlarm(enemy).getLevel();
if (entry.getValue() != newLevel) {
cs.add(See.only(null).perhaps((ServerPlayer) enemy),
settlement);
}
}
}
// Check for braves converted by missionaries
List<UnitType> converts = game.getSpecification()
.getUnitTypesWithAbility("model.ability.convert");
StringTemplate nation = getNationName();
for (IndianSettlement settlement : allSettlements) {
if (settlement.checkForNewMissionaryConvert()) {
Unit missionary = settlement.getMissionary();
ServerPlayer other = (ServerPlayer) missionary.getOwner();
Settlement colony = settlement.getTile()
.getNearestSettlement(other, MAX_CONVERT_DISTANCE);
if (colony != null && converts.size() > 0) {
Unit brave = settlement.getUnitList().get(0);
brave.clearEquipment();
brave.setOwner(other);
brave.setIndianSettlement(null);
brave.setNationality(other.getNationID());
brave.setType(Utils.getRandomMember(logger,
"Choose brave", converts, random));
brave.setLocation(colony.getTile());
cs.add(See.perhaps(), colony.getTile(), settlement);
cs.addMessage(See.only(other),
new ModelMessage(ModelMessage.MessageType.UNIT_ADDED,
"model.colony.newConvert", brave)
.addStringTemplate("%nation%", nation)
.addName("%colony%", colony.getName()));
}
}
}
}
}
/**
* All player colonies bombard all available targets.
*
* @param random A random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csBombardEnemyShips(Random random, ChangeSet cs) {
for (Colony colony : getColonies()) {
if (colony.canBombardEnemyShip()) {
for (Tile tile : colony.getTile().getSurroundingTiles(1)) {
if (!tile.isLand() && tile.getFirstUnit() != null
&& tile.getFirstUnit().getOwner() != this) {
for (Unit unit : tile.getUnitList()) {
if (atWarWith(unit.getOwner())
|| unit.hasAbility(Ability.PIRACY)) {
csCombat(colony, unit, null, random, cs);
}
}
}
}
}
}
}
/**
* Add or remove a standard yearly amount of storable goods, and a
* random extra amount of a random type.
*
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csYearlyGoodsAdjust(Random random, ChangeSet cs) {
List<GoodsType> goodsTypes = getGame().getSpecification()
.getGoodsTypeList();
Market market = getMarket();
// Pick a random type of storable goods to add/remove an extra
// amount of.
GoodsType extraType;
while (!(extraType = Utils.getRandomMember(logger, "Choose goods type",
goodsTypes, random))
.isStorable());
// Remove standard amount, and the extra amount.
for (GoodsType type : goodsTypes) {
if (type.isStorable() && market.hasBeenTraded(type)) {
boolean add = market.getAmountInMarket(type)
< type.getInitialAmount();
int amount = getGame().getTurn().getNumber() / 10;
if (type == extraType) amount = 2 * amount + 1;
if (amount <= 0) continue;
amount = Utils.randomInt(logger, "Market adjust " + type,
random, amount);
if (!add) amount = -amount;
market.addGoodsToMarket(type, amount);
logger.finest(getName() + " adjust of " + amount
+ " " + type
+ ", total: " + market.getAmountInMarket(type)
+ ", initial: " + type.getInitialAmount());
csFlushMarket(type, cs);
}
}
}
/**
* Adds a founding father to a players continental congress.
*
* @param father The <code>FoundingFather</code> to add.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csAddFoundingFather(FoundingFather father, Random random,
ChangeSet cs) {
Game game = getGame();
Specification spec = game.getSpecification();
Europe europe = getEurope();
boolean europeDirty = false;
// TODO: We do not want to have to update the whole player
// just to get the FF into the client. Use this hack until
// the client gets proper containers.
cs.addFather(this, father);
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.SONS_OF_LIBERTY,
"model.player.foundingFatherJoinedCongress",
this)
.add("%foundingFather%", father.getNameKey())
.add("%description%", father.getDescriptionKey()));
cs.addHistory(this,
new HistoryEvent(getGame().getTurn(),
HistoryEvent.EventType.FOUNDING_FATHER)
.add("%father%", father.getNameKey()));
List<AbstractUnit> units = father.getUnits();
if (units != null && !units.isEmpty() && europe != null) {
createUnits(father.getUnits(), europe);
europeDirty = true;
}
java.util.Map<UnitType, UnitType> upgrades = father.getUpgrades();
if (upgrades != null) {
for (Unit u : getUnits()) {
UnitType newType = upgrades.get(u.getType());
if (newType != null) {
u.setType(newType);
cs.add(See.perhaps(), u);
}
}
}
if (recalculateBellsBonus()) {
cs.add(See.only(this), this);
}
for (Event event : father.getEvents()) {
String eventId = event.getId();
if (eventId.equals("model.event.resetNativeAlarm")) {
for (Player p : game.getPlayers()) {
if (!p.isDead() && p.isIndian() && p.hasContacted(this)) {
p.setTension(this, new Tension(Tension.TENSION_MIN));
for (IndianSettlement is : p.getIndianSettlements()) {
if (is.hasContactedSettlement(this)) {
is.setAlarm(this,
new Tension(Tension.TENSION_MIN));
cs.add(See.only(this), is);
}
}
csChangeStance(Stance.PEACE, p, true, cs);
}
}
} else if (eventId.equals("model.event.boycottsLifted")) {
Market market = getMarket();
for (GoodsType goodsType : spec.getGoodsTypeList()) {
if (market.getArrears(goodsType) > 0) {
market.setArrears(goodsType, 0);
cs.add(See.only(this), market.getMarketData(goodsType));
}
}
} else if (eventId.equals("model.event.freeBuilding")) {
BuildingType type = spec.getBuildingType(event.getValue());
for (Colony colony : getColonies()) {
if (colony.canBuild(type)) {
colony.addBuilding(new ServerBuilding(game, colony, type));
colony.getBuildQueue().remove(type);
cs.add(See.only(this), colony);
if (isAI()) {
colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false);
}
}
}
} else if (eventId.equals("model.event.seeAllColonies")) {
for (Tile t : game.getMap().getAllTiles()) {
Colony colony = t.getColony();
if (colony != null
&& (ServerPlayer) colony.getOwner() != this) {
if (!t.isExploredBy(this)) {
t.setExploredBy(this, true);
}
t.updatePlayerExploredTile(this, false);
cs.add(See.only(this), t);
for (Tile x : colony.getOwnedTiles()) {
if (!x.isExploredBy(this)) {
x.setExploredBy(this, true);
}
x.updatePlayerExploredTile(this, false);
cs.add(See.only(this), x);
}
}
}
} else if (eventId.equals("model.event.increaseSonsOfLiberty")) {
int value = Integer.parseInt(event.getValue());
GoodsType bells = spec.getLibertyGoodsTypeList().get(0);
int totalBells = 0;
for (Colony colony : getColonies()) {
float oldRatio = (float) colony.getLiberty()
/ (colony.getUnitCount() * Colony.LIBERTY_PER_REBEL);
float reqRatio = Math.min(1.0f, oldRatio + 0.01f * value);
int reqBells = (int) Math.round(Colony.LIBERTY_PER_REBEL
* colony.getUnitCount()
* (reqRatio - oldRatio));
if (reqBells > 0) { // Can go negative if already over 100%
colony.addGoods(bells, reqBells);
colony.updateSoL();
cs.add(See.only(this), colony);
totalBells += reqBells;
}
}
// Bonus bells from the FF do not count towards recruiting
// the next one!
incrementLiberty(-totalBells);
} else if (eventId.equals("model.event.newRecruits")
&& europe != null) {
List<RandomChoice<UnitType>> recruits
= generateRecruitablesList();
for (int i = 0; i < Europe.RECRUIT_COUNT; i++) {
if (!hasAbility("model.ability.canRecruitUnit",
europe.getRecruitable(i))) {
UnitType newType = RandomChoice
.getWeightedRandom(logger,
"Replace recruit", random, recruits);
europe.setRecruitable(i, newType);
europeDirty = true;
}
}
} else if (eventId.equals("model.event.movementChange")) {
for (Unit u : getUnits()) {
if (u.getMovesLeft() > 0) {
u.setMovesLeft(u.getInitialMovesLeft());
cs.addPartial(See.only(this), u, "movesLeft");
}
}
}
}
if (europeDirty) cs.add(See.only(this), europe);
}
/**
* Claim land.
*
* @param tile The <code>Tile</code> to claim.
* @param settlement The <code>Settlement</code> to claim for.
* @param price The price to pay for the land, which must agree
* with the owner valuation, unless negative which denotes stealing.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csClaimLand(Tile tile, Settlement settlement, int price,
ChangeSet cs) {
Player owner = tile.getOwner();
Settlement ownerSettlement = tile.getOwningSettlement();
tile.changeOwnership(this, settlement);
// Update the tile for all, and privately any now-angrier
// owners, or the player gold if a price was paid.
cs.add(See.perhaps(), tile);
if (price > 0) {
modifyGold(-price);
owner.modifyGold(price);
cs.addPartial(See.only(this), this, "gold");
} else if (price < 0 && owner.isIndian()) {
IndianSettlement is = (IndianSettlement) ownerSettlement;
if (is != null) {
cs.add(See.only(null).perhaps(this),
owner.modifyTension(this, Tension.TENSION_ADD_LAND_TAKEN, is));
}
}
}
/**
* A unit migrates from Europe.
*
* @param slot The slot within <code>Europe</code> to select the unit from.
* @param type The type of migration occurring.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csEmigrate(int slot, MigrationType type, Random random,
ChangeSet cs) {
// Valid slots are in [1,3], recruitable indices are in [0,2].
// An invalid slot is normal when the player has no control over
// recruit type.
boolean selected = 1 <= slot && slot <= Europe.RECRUIT_COUNT;
int index = (selected) ? slot-1
: Utils.randomInt(logger, "Choose emigrant", random,
Europe.RECRUIT_COUNT);
// Create the recruit, move it to the docks.
Europe europe = getEurope();
UnitType recruitType = europe.getRecruitable(index);
Game game = getGame();
Unit unit = new ServerUnit(game, europe, this, recruitType);
unit.setLocation(europe);
// Handle migration type specific changes.
switch (type) {
case FOUNTAIN:
setRemainingEmigrants(getRemainingEmigrants() - 1);
break;
case RECRUIT:
modifyGold(-europe.getRecruitPrice());
cs.addPartial(See.only(this), this, "gold");
europe.increaseRecruitmentDifficulty();
// Fall through
case NORMAL:
updateImmigrationRequired();
reduceImmigration();
cs.addPartial(See.only(this), this,
"immigration", "immigrationRequired");
break;
case SURVIVAL:
break;
default:
throw new IllegalArgumentException("Bogus migration type");
}
// Replace the recruit we used. Shuffle them down first
// as AI is always recruiting slot 0.
for (int i = index; i < Europe.RECRUIT_COUNT-1; i++) {
europe.setRecruitable(i, europe.getRecruitable(i+1));
}
List<RandomChoice<UnitType>> recruits = generateRecruitablesList();
europe.setRecruitable(Europe.RECRUIT_COUNT-1,
RandomChoice.getWeightedRandom(logger,
"Replace recruit", random, recruits));
cs.add(See.only(this), europe);
// Add an informative message if this was a survival recruitment,
// or an ordinary migration where we did not select the unit type.
// Other cases were selected.
if (type == MigrationType.SURVIVAL) {
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.UNIT_ADDED,
"model.europe.autoRecruit",
this, unit)
.add("%europe%", europe.getNameKey())
.addStringTemplate("%unit%", unit.getLabel()));
} else if (!selected) {
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.UNIT_ADDED,
"model.europe.emigrate",
this, unit)
.add("%europe%", europe.getNameKey())
.addStringTemplate("%unit%", unit.getLabel()));
}
}
/**
* Combat.
*
* @param attacker The <code>FreeColGameObject</code> that is attacking.
* @param defender The <code>FreeColGameObject</code> that is defending.
* @param crs A list of <code>CombatResult</code>s defining the result.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csCombat(FreeColGameObject attacker,
FreeColGameObject defender,
List<CombatResult> crs,
Random random,
ChangeSet cs) throws IllegalStateException {
CombatModel combatModel = getGame().getCombatModel();
boolean isAttack = combatModel.combatIsAttack(attacker, defender);
boolean isBombard = combatModel.combatIsBombard(attacker, defender);
Unit attackerUnit = null;
Settlement attackerSettlement = null;
Tile attackerTile = null;
Unit defenderUnit = null;
ServerPlayer defenderPlayer = null;
Tile defenderTile = null;
if (isAttack) {
attackerUnit = (Unit) attacker;
attackerTile = attackerUnit.getTile();
defenderUnit = (Unit) defender;
defenderPlayer = (ServerPlayer) defenderUnit.getOwner();
defenderTile = defenderUnit.getTile();
boolean bombard = attackerUnit.hasAbility(Ability.BOMBARD);
cs.addAttribute(See.only(this), "sound",
(attackerUnit.isNaval()) ? "sound.attack.naval"
: (bombard) ? "sound.attack.artillery"
: (attackerUnit.isMounted()) ? "sound.attack.mounted"
: "sound.attack.foot");
if (attackerUnit.getOwner().isIndian()
&& defenderPlayer.isEuropean()
&& defenderUnit.getLocation().getColony() != null
&& !defenderPlayer.atWarWith(attackerUnit.getOwner())) {
StringTemplate attackerNation
= attackerUnit.getApparentOwnerName();
Colony colony = defenderUnit.getLocation().getColony();
cs.addMessage(See.only(defenderPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.indianSurprise", colony)
.addStringTemplate("%nation%", attackerNation)
.addName("%colony%", colony.getName()));
}
} else if (isBombard) {
attackerSettlement = (Settlement) attacker;
attackerTile = attackerSettlement.getTile();
defenderUnit = (Unit) defender;
defenderPlayer = (ServerPlayer) defenderUnit.getOwner();
defenderTile = defenderUnit.getTile();
cs.addAttribute(See.only(this), "sound", "sound.attack.bombard");
} else {
throw new IllegalStateException("Bogus combat");
}
// If the combat results were not specified (usually the case),
// query the combat model.
if (crs == null) {
crs = combatModel.generateAttackResult(random, attacker, defender);
}
if (crs.isEmpty()) {
throw new IllegalStateException("empty attack result");
}
// Extract main result, insisting it is one of the fundamental cases,
// and add the animation.
// Set vis so that loser always sees things.
// TODO: Bombard animations
See vis; // Visibility that insists on the loser seeing the result.
CombatResult result = crs.remove(0);
switch (result) {
case NO_RESULT:
vis = See.perhaps();
break; // Do not animate if there is no result.
case WIN:
vis = See.perhaps().always(defenderPlayer);
if (isAttack) {
cs.addAttack(vis, attackerUnit, defenderUnit, true);
}
break;
case LOSE:
vis = See.perhaps().always(this);
if (isAttack) {
cs.addAttack(vis, attackerUnit, defenderUnit, false);
}
break;
default:
throw new IllegalStateException("generateAttackResult returned: "
+ result);
}
// Now process the details.
boolean attackerTileDirty = false;
boolean defenderTileDirty = false;
boolean moveAttacker = false;
boolean burnedNativeCapital = false;
Settlement settlement = defenderTile.getSettlement();
Colony colony = defenderTile.getColony();
IndianSettlement natives = (settlement instanceof IndianSettlement)
? (IndianSettlement) settlement
: null;
int attackerTension = 0;
int defenderTension = 0;
for (CombatResult cr : crs) {
boolean ok;
switch (cr) {
case AUTOEQUIP_UNIT:
ok = isAttack && settlement != null;
if (ok) {
csAutoequipUnit(defenderUnit, settlement, cs);
}
break;
case BURN_MISSIONS:
ok = isAttack && result == CombatResult.WIN
&& natives != null
&& isEuropean() && defenderPlayer.isIndian();
if (ok) {
defenderTileDirty |= natives.getMissionary(this) != null;
csBurnMissions(attackerUnit, natives, cs);
}
break;
case CAPTURE_AUTOEQUIP:
ok = isAttack && result == CombatResult.WIN
&& settlement != null
&& defenderPlayer.isEuropean();
if (ok) {
csCaptureAutoEquip(attackerUnit, defenderUnit, cs);
attackerTileDirty = defenderTileDirty = true;
}
break;
case CAPTURE_COLONY:
ok = isAttack && result == CombatResult.WIN
&& colony != null
&& isEuropean() && defenderPlayer.isEuropean();
if (ok) {
csCaptureColony(attackerUnit, colony, random, cs);
attackerTileDirty = defenderTileDirty = false;
moveAttacker = true;
defenderTension += Tension.TENSION_ADD_MAJOR;
}
break;
case CAPTURE_CONVERT:
ok = isAttack && result == CombatResult.WIN
&& natives != null
&& isEuropean() && defenderPlayer.isIndian();
if (ok) {
csCaptureConvert(attackerUnit, natives, random, cs);
attackerTileDirty = true;
}
break;
case CAPTURE_EQUIP:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csCaptureEquip(attackerUnit, defenderUnit, cs);
} else {
csCaptureEquip(defenderUnit, attackerUnit, cs);
}
attackerTileDirty = defenderTileDirty = true;
}
break;
case CAPTURE_UNIT:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csCaptureUnit(attackerUnit, defenderUnit, cs);
} else {
csCaptureUnit(defenderUnit, attackerUnit, cs);
}
attackerTileDirty = defenderTileDirty = true;
}
break;
case DAMAGE_COLONY_SHIPS:
ok = isAttack && result == CombatResult.WIN
&& colony != null;
if (ok) {
csDamageColonyShips(attackerUnit, colony, cs);
defenderTileDirty = true;
}
break;
case DAMAGE_SHIP_ATTACK:
ok = isAttack && result != CombatResult.NO_RESULT
&& ((result == CombatResult.WIN) ? defenderUnit
: attackerUnit).isNaval();
if (ok) {
if (result == CombatResult.WIN) {
csDamageShipAttack(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
} else {
csDamageShipAttack(defenderUnit, attackerUnit, cs);
attackerTileDirty = true;
}
}
break;
case DAMAGE_SHIP_BOMBARD:
ok = isBombard && result == CombatResult.WIN
&& defenderUnit.isNaval();
if (ok) {
csDamageShipBombard(attackerSettlement, defenderUnit, cs);
defenderTileDirty = true;
}
break;
case DEMOTE_UNIT:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csDemoteUnit(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
} else {
csDemoteUnit(defenderUnit, attackerUnit, cs);
attackerTileDirty = true;
}
}
break;
case DESTROY_COLONY:
ok = isAttack && result == CombatResult.WIN
&& colony != null
&& isIndian() && defenderPlayer.isEuropean();
if (ok) {
csDestroyColony(attackerUnit, colony, random, cs);
attackerTileDirty = defenderTileDirty = true;
moveAttacker = true;
attackerTension -= Tension.TENSION_ADD_NORMAL;
defenderTension += Tension.TENSION_ADD_MAJOR;
}
break;
case DESTROY_SETTLEMENT:
ok = isAttack && result == CombatResult.WIN
&& natives != null
&& defenderPlayer.isIndian();
if (ok) {
burnedNativeCapital = settlement.isCapital();
csDestroySettlement(attackerUnit, natives, random, cs);
attackerTileDirty = defenderTileDirty = true;
moveAttacker = true;
attackerTension -= Tension.TENSION_ADD_NORMAL;
if (!burnedNativeCapital) {
defenderTension += Tension.TENSION_ADD_MAJOR;
}
}
break;
case EVADE_ATTACK:
ok = isAttack && result == CombatResult.NO_RESULT
&& defenderUnit.isNaval();
if (ok) {
csEvadeAttack(attackerUnit, defenderUnit, cs);
}
break;
case EVADE_BOMBARD:
ok = isBombard && result == CombatResult.NO_RESULT
&& defenderUnit.isNaval();
if (ok) {
csEvadeBombard(attackerSettlement, defenderUnit, cs);
}
break;
case LOOT_SHIP:
ok = isAttack && result != CombatResult.NO_RESULT
&& attackerUnit.isNaval() && defenderUnit.isNaval();
if (ok) {
if (result == CombatResult.WIN) {
csLootShip(attackerUnit, defenderUnit, cs);
} else {
csLootShip(defenderUnit, attackerUnit, cs);
}
}
break;
case LOSE_AUTOEQUIP:
ok = isAttack && result == CombatResult.WIN
&& settlement != null
&& defenderPlayer.isEuropean();
if (ok) {
csLoseAutoEquip(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
}
break;
case LOSE_EQUIP:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csLoseEquip(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
} else {
csLoseEquip(defenderUnit, attackerUnit, cs);
attackerTileDirty = true;
}
}
break;
case PILLAGE_COLONY:
ok = isAttack && result == CombatResult.WIN
&& colony != null
&& isIndian() && defenderPlayer.isEuropean();
if (ok) {
csPillageColony(attackerUnit, colony, random, cs);
defenderTileDirty = true;
attackerTension -= Tension.TENSION_ADD_NORMAL;
}
break;
case PROMOTE_UNIT:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csPromoteUnit(attackerUnit, defenderUnit, cs);
attackerTileDirty = true;
} else {
csPromoteUnit(defenderUnit, attackerUnit, cs);
defenderTileDirty = true;
}
}
break;
case SINK_COLONY_SHIPS:
ok = isAttack && result == CombatResult.WIN
&& colony != null;
if (ok) {
csSinkColonyShips(attackerUnit, colony, cs);
defenderTileDirty = true;
}
break;
case SINK_SHIP_ATTACK:
ok = isAttack && result != CombatResult.NO_RESULT
&& ((result == CombatResult.WIN) ? defenderUnit
: attackerUnit).isNaval();
if (ok) {
if (result == CombatResult.WIN) {
csSinkShipAttack(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
} else {
csSinkShipAttack(defenderUnit, attackerUnit, cs);
attackerTileDirty = true;
}
}
break;
case SINK_SHIP_BOMBARD:
ok = isBombard && result == CombatResult.WIN
&& defenderUnit.isNaval();
if (ok) {
csSinkShipBombard(attackerSettlement, defenderUnit, cs);
defenderTileDirty = true;
}
break;
case SLAUGHTER_UNIT:
ok = isAttack && result != CombatResult.NO_RESULT;
if (ok) {
if (result == CombatResult.WIN) {
csSlaughterUnit(attackerUnit, defenderUnit, cs);
defenderTileDirty = true;
attackerTension -= Tension.TENSION_ADD_NORMAL;
defenderTension += getSlaughterTension(defenderUnit);
} else {
csSlaughterUnit(defenderUnit, attackerUnit, cs);
attackerTileDirty = true;
attackerTension += getSlaughterTension(attackerUnit);
defenderTension -= Tension.TENSION_ADD_NORMAL;
}
}
break;
default:
ok = false;
break;
}
if (!ok) {
throw new IllegalStateException("Attack (result=" + result
+ ") has bogus subresult: "
+ cr);
}
}
// Handle stance and tension.
// - Privateers do not provoke stance changes but can set the
// attackedByPrivateers flag
// - Attacks among Europeans imply war
// - Burning of a native capital results in surrender
// - Other attacks involving natives do not imply war, but
// changes in Tension can drive Stance, however this is
// decided by the native AI in their turn so just adjust tension.
if (attacker.hasAbility(Ability.PIRACY)) {
if (!defenderPlayer.getAttackedByPrivateers()) {
defenderPlayer.setAttackedByPrivateers(true);
cs.addPartial(See.only(defenderPlayer), defenderPlayer,
"attackedByPrivateers");
}
} else if (defender.hasAbility(Ability.PIRACY)) {
; // do nothing
} else if (burnedNativeCapital) {
defenderPlayer.getTension(this).setValue(Tension.SURRENDERED);
cs.add(See.perhaps().always(this), defenderPlayer); // TODO: just the tension
csChangeStance(Stance.PEACE, defenderPlayer, true, cs);
for (IndianSettlement is : defenderPlayer.getIndianSettlements()) {
if (is.hasContactedSettlement(this)) {
is.getAlarm(this).setValue(Tension.SURRENDERED);
// Only update attacker with settlements that have
// been seen, as contact can occur with its members.
if (is.getTile().isExploredBy(this)) {
cs.add(See.perhaps().always(this), is);
} else {
cs.add(See.only(defenderPlayer), is);
}
}
}
} else if (isEuropean() && defenderPlayer.isEuropean()) {
csChangeStance(Stance.WAR, defenderPlayer, true, cs);
} else { // At least one player is non-European
if (isEuropean()) {
csChangeStance(Stance.WAR, defenderPlayer, true, cs);
} else if (isIndian()) {
if (result == CombatResult.WIN) {
attackerTension -= Tension.TENSION_ADD_MINOR;
} else if (result == CombatResult.LOSE) {
attackerTension += Tension.TENSION_ADD_MINOR;
}
}
if (defenderPlayer.isEuropean()) {
defenderPlayer.csChangeStance(Stance.WAR, this, true, cs);
} else if (defenderPlayer.isIndian()) {
if (result == CombatResult.WIN) {
defenderTension += Tension.TENSION_ADD_MINOR;
} else if (result == CombatResult.LOSE) {
defenderTension -= Tension.TENSION_ADD_MINOR;
}
}
if (attackerTension != 0) {
cs.add(See.only(null).perhaps(defenderPlayer),
modifyTension(defenderPlayer, attackerTension));
}
if (defenderTension != 0) {
cs.add(See.only(null).perhaps(this),
defenderPlayer.modifyTension(this, defenderTension));
}
}
// Move the attacker if required.
if (moveAttacker) {
attackerUnit.setMovesLeft(attackerUnit.getInitialMovesLeft());
((ServerUnit) attackerUnit).csMove(defenderTile, random, cs);
attackerUnit.setMovesLeft(0);
// Move adds in updates for the tiles, but...
attackerTileDirty = defenderTileDirty = false;
// ...with visibility of perhaps().
// Thus the defender might see the change,
// but because its settlement is gone it also might not.
// So add in another defender-specific update.
// The worst that can happen is a duplicate update.
cs.add(See.only(defenderPlayer), defenderTile);
} else if (isAttack) {
// The Revenger unit can attack multiple times, so spend
// at least the eventual cost of moving to the tile.
// Other units consume the entire move.
if (attacker.hasAbility("model.ability.multipleAttacks")) {
int movecost = attackerUnit.getMoveCost(defenderTile);
attackerUnit.setMovesLeft(attackerUnit.getMovesLeft()
- movecost);
} else {
attackerUnit.setMovesLeft(0);
}
if (!attackerTileDirty) {
cs.addPartial(See.only(this), attacker, "movesLeft");
}
}
// Make sure we always update the attacker and defender tile
// if it is not already done yet.
if (attackerTileDirty) cs.add(vis, attackerTile);
if (defenderTileDirty) cs.add(vis, defenderTile);
}
/**
* Gets the amount to raise tension by when a unit is slaughtered.
*
* @param loser The <code>Unit</code> that dies.
* @return An amount to raise tension by.
*/
private int getSlaughterTension(Unit loser) {
// Tension rises faster when units die.
Settlement settlement = loser.getSettlement();
if (settlement != null) {
if (settlement instanceof IndianSettlement) {
return (((IndianSettlement) settlement).isCapital())
? Tension.TENSION_ADD_CAPITAL_ATTACKED
: Tension.TENSION_ADD_SETTLEMENT_ATTACKED;
} else {
return Tension.TENSION_ADD_NORMAL;
}
} else { // attack in the open
return (loser.getIndianSettlement() != null)
? Tension.TENSION_ADD_UNIT_DESTROYED
: Tension.TENSION_ADD_MINOR;
}
}
/**
* Notifies of automatic arming.
*
* @param unit The <code>Unit</code> that is auto-equipping.
* @param settlement The <code>Settlement</code> being defended.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csAutoequipUnit(Unit unit, Settlement settlement,
ChangeSet cs) {
ServerPlayer player = (ServerPlayer) unit.getOwner();
cs.addMessage(See.only(player),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.automaticDefence", unit)
.addStringTemplate("%unit%", unit.getLabel())
.addName("%colony%", settlement.getName()));
}
/**
* Burns a players missions.
*
* @param attacker The <code>Unit</code> that attacked.
* @param settlement The <code>IndianSettlement</code> that was attacked.
* @param cs The <code>ChangeSet</code> to update.
*/
private void csBurnMissions(Unit attacker, IndianSettlement settlement,
ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attackerPlayer.getNationName();
ServerPlayer nativePlayer = (ServerPlayer) settlement.getOwner();
StringTemplate nativeNation = nativePlayer.getNationName();
// Message only for the European player
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.burnMissions", attacker, settlement)
.addStringTemplate("%nation%", attackerNation)
.addStringTemplate("%enemyNation%", nativeNation));
// Burn down the missions
for (IndianSettlement s : nativePlayer.getIndianSettlements()) {
Unit missionary = s.getMissionary(attackerPlayer);
if (missionary != null) {
s.changeMissionary(null);
if (s != settlement) cs.add(See.perhaps(), s.getTile());
}
}
}
/**
* Defender autoequips but loses and attacker captures the equipment.
*
* @param attacker The <code>Unit</code> that attacked.
* @param defender The <code>Unit</code> that defended and loses equipment.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csCaptureAutoEquip(Unit attacker, Unit defender,
ChangeSet cs) {
EquipmentType equip
= defender.getBestCombatEquipmentType(defender.getAutomaticEquipment());
csLoseAutoEquip(attacker, defender, cs);
csCaptureEquipment(attacker, defender, equip, cs);
}
/**
* Captures a colony.
*
* @param attacker The attacking <code>Unit</code>.
* @param colony The <code>Colony</code> to capture.
* @param random A pseudo-random number source.
* @param cs The <code>ChangeSet</code> to update.
*/
private void csCaptureColony(Unit attacker, Colony colony, Random random,
ChangeSet cs) {
Game game = attacker.getGame();
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attackerPlayer.getNationName();
ServerPlayer colonyPlayer = (ServerPlayer) colony.getOwner();
StringTemplate colonyNation = colonyPlayer.getNationName();
Tile tile = colony.getTile();
int plunder = colony.getPlunder(attacker, random);
// Handle history and messages before colony handover
cs.addHistory(attackerPlayer,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.CONQUER_COLONY)
.addStringTemplate("%nation%", colonyNation)
.addName("%colony%", colony.getName()));
cs.addHistory(colonyPlayer,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.COLONY_CONQUERED)
.addStringTemplate("%nation%", attackerNation)
.addName("%colony%", colony.getName()));
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.colonyCaptured",
colony)
.addName("%colony%", colony.getName())
.addAmount("%amount%", plunder));
cs.addMessage(See.only(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.colonyCapturedBy",
colony.getTile())
.addName("%colony%", colony.getName())
.addAmount("%amount%", plunder)
.addStringTemplate("%player%", attackerNation));
// Allocate some plunder
if (plunder > 0) {
attackerPlayer.modifyGold(plunder);
colonyPlayer.modifyGold(-plunder);
cs.addPartial(See.only(attackerPlayer), attackerPlayer, "gold");
cs.addPartial(See.only(colonyPlayer), colonyPlayer, "gold");
}
// Hand over the colony.
colony.changeOwner(attackerPlayer);
// Remove goods party modifiers as they apply to a different monarch.
for (Modifier m : colony.getModifiers()) {
if ("model.modifier.colonyGoodsParty".equals(m.getSource())) {
colony.removeModifier(m);
}
}
// Inform former owner of loss of owned tiles, and process possible
// increase in line of sight. Leave other exploration etc to csMove.
for (Tile t : colony.getOwnedTiles()) {
cs.add(See.perhaps().always(colonyPlayer), t);
}
if (colony.getLineOfSight() > attacker.getLineOfSight()) {
for (Tile t : tile.getSurroundingTiles(attacker.getLineOfSight(),
colony.getLineOfSight())) {
// New owner has now explored within settlement line of sight.
attackerPlayer.setExplored(t);
cs.add(See.only(attackerPlayer), t);
}
}
// Inform the former owner of loss of units. Only the potentially
// active units on the tile need be considered. Carried and colony
// workers will just become inaccessible.
for (Unit u : tile.getUnitList()) {
cs.addDisappear(colonyPlayer, tile, u);
}
cs.addAttribute(See.only(attackerPlayer), "sound",
"sound.event.captureColony");
}
/**
* Extracts a convert from a native settlement.
*
* @param attacker The <code>Unit</code> that is attacking.
* @param natives The <code>IndianSettlement</code> under attack.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csCaptureConvert(Unit attacker, IndianSettlement natives,
Random random, ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate convertNation = natives.getOwner().getNationName();
List<UnitType> converts = getGame().getSpecification()
.getUnitTypesWithAbility("model.ability.convert");
UnitType type = Utils.getRandomMember(logger, "Choose convert",
converts, random);
Unit convert = natives.getUnitList().get(0);
convert.clearEquipment();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.newConvertFromAttack",
convert)
.addStringTemplate("%nation%", convertNation)
.addStringTemplate("%unit%", convert.getLabel()));
convert.setOwner(attacker.getOwner());
// do not change nationality: convert was forcibly captured and wants to run away
convert.setType(type);
convert.setLocation(attacker.getTile());
}
/**
* Captures equipment.
*
* @param winner The <code>Unit</code> that captures equipment.
* @param loser The <code>Unit</code> that defended and loses equipment.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csCaptureEquip(Unit winner, Unit loser, ChangeSet cs) {
EquipmentType equip
= loser.getBestCombatEquipmentType(loser.getEquipment());
csLoseEquip(winner, loser, cs);
csCaptureEquipment(winner, loser, equip, cs);
}
/**
* Capture equipment.
*
* @param winner The <code>Unit</code> that is capturing equipment.
* @param loser The <code>Unit</code> that is losing equipment.
* @param equip The <code>EquipmentType</code> to capture.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csCaptureEquipment(Unit winner, Unit loser,
EquipmentType equip, ChangeSet cs) {
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
ServerPlayer loserPlayer = (ServerPlayer) loser.getOwner();
if ((equip = winner.canCaptureEquipment(equip, loser)) != null) {
// TODO: what if winner captures equipment that is
// incompatible with their current equipment?
// Currently, can-not-happen, so ignoring the return from
// changeEquipment. Beware.
winner.changeEquipment(equip, 1);
// Currently can not capture equipment back so this only
// makes sense for native players, and the message is
// native specific.
if (winnerPlayer.isIndian()) {
StringTemplate winnerNation = winnerPlayer.getNationName();
cs.addMessage(See.only(loserPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.equipmentCaptured",
winnerPlayer)
.addStringTemplate("%nation%", winnerNation)
.add("%equipment%", equip.getNameKey()));
// CHEAT: Immediately transferring the captured goods
// back to a potentially remote settlement is pretty
// dubious. Apparently Col1 did it. Better would be
// to give the capturing unit a go-home-with-plunder mission.
IndianSettlement settlement = winner.getIndianSettlement();
if (settlement != null) {
for (AbstractGoods goods : equip.getGoodsRequired()) {
settlement.addGoods(goods);
logger.finest("CHEAT teleporting " + goods.toString()
+ " back to " + settlement.getName());
}
cs.add(See.only(winnerPlayer), settlement);
}
}
}
}
/**
* Capture a unit.
*
* @param winner A <code>Unit</code> that is capturing.
* @param loser A <code>Unit</code> to capture.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csCaptureUnit(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer loserPlayer = (ServerPlayer) loser.getOwner();
StringTemplate loserNation = loserPlayer.getNationName();
StringTemplate loserLocation = loser.getLocation()
.getLocationNameFor(loserPlayer);
StringTemplate oldName = loser.getLabel();
String messageId = loser.getType().getId() + ".captured";
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
StringTemplate winnerNation = winnerPlayer.getNationName();
StringTemplate winnerLocation = winner.getLocation()
.getLocationNameFor(winnerPlayer);
// Capture the unit
UnitType type = loser.getTypeChange((winnerPlayer.isUndead())
? ChangeType.UNDEAD
: ChangeType.CAPTURE, winnerPlayer);
loser.setOwner(winnerPlayer);
if (type != null) loser.setType(type);
loser.setLocation(winner.getTile());
loser.setState(Unit.UnitState.ACTIVE);
// Winner message post-capture when it owns the loser
cs.addMessage(See.only(winnerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, loser)
.setDefaultId("model.unit.unitCaptured")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%unit%", oldName)
.addStringTemplate("%enemyNation%", winnerNation)
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", winnerLocation));
cs.addMessage(See.only(loserPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, loser.getTile())
.setDefaultId("model.unit.unitCaptured")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%unit%", oldName)
.addStringTemplate("%enemyNation%", winnerNation)
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", loserLocation));
}
/**
* Damages all ships in a colony in preparation for capture.
*
* @param attacker The <code>Unit</code> that is damaging.
* @param colony The <code>Colony</code> to damage ships in.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDamageColonyShips(Unit attacker, Colony colony,
ChangeSet cs) {
List<Unit> units = colony.getTile().getUnitList();
while (!units.isEmpty()) {
Unit unit = units.remove(0);
if (unit.isNaval()) csDamageShipAttack(attacker, unit, cs);
}
}
/**
* Damage a ship through normal attack.
*
* @param attacker The attacker <code>Unit</code>.
* @param ship The <code>Unit</code> which is a ship to damage.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDamageShipAttack(Unit attacker, Unit ship, ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attacker.getApparentOwnerName();
ServerPlayer shipPlayer = (ServerPlayer) ship.getOwner();
Location repair = ship.getRepairLocation();
StringTemplate repairLoc = repair.getLocationNameFor(shipPlayer);
StringTemplate shipNation = ship.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.enemyShipDamaged", attacker)
.addStringTemplate("%unit%", attacker.getLabel())
.addStringTemplate("%enemyNation%", shipNation)
.addStringTemplate("%enemyUnit%", ship.getLabel()));
cs.addMessage(See.only(shipPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipDamaged", ship)
.addStringTemplate("%unit%", ship.getLabel())
.addStringTemplate("%enemyUnit%", attacker.getLabel())
.addStringTemplate("%enemyNation%", attackerNation)
.addStringTemplate("%repairLocation%", repairLoc));
csDamageShip(ship, repair, cs);
}
/**
* Damage a ship through bombard.
*
* @param settlement The attacker <code>Settlement</code>.
* @param ship The <code>Unit</code> which is a ship to damage.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDamageShipBombard(Settlement settlement, Unit ship,
ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) settlement.getOwner();
ServerPlayer shipPlayer = (ServerPlayer) ship.getOwner();
Location repair = ship.getRepairLocation();
StringTemplate repairLoc = repair.getLocationNameFor(shipPlayer);
StringTemplate shipNation = ship.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.enemyShipDamagedByBombardment", settlement)
.addName("%colony%", settlement.getName())
.addStringTemplate("%nation%", shipNation)
.addStringTemplate("%unit%", ship.getLabel()));
cs.addMessage(See.only(shipPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipDamagedByBombardment", ship)
.addName("%colony%", settlement.getName())
.addStringTemplate("%unit%", ship.getLabel())
.addStringTemplate("%repairLocation%", repairLoc));
csDamageShip(ship, repair, cs);
}
/**
* Damage a ship.
*
* @param ship The naval <code>Unit</code> to damage.
* @param repair The <code>Location</code> to send it to.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDamageShip(Unit ship, Location repair, ChangeSet cs) {
ServerPlayer player = (ServerPlayer) ship.getOwner();
// Lose the goods and units aboard
for (Goods g : ship.getGoodsContainer().getCompactGoods()) {
ship.remove(g);
}
for (Unit u : ship.getUnitList()) {
ship.remove(u);
cs.addDispose(See.only(player), null, u); // Only owner-visible
}
// Damage the ship and send it off for repair
ship.setHitpoints(1);
ship.setDestination(null);
ship.setLocation(repair);
ship.setState(Unit.UnitState.ACTIVE);
ship.setMovesLeft(0);
cs.add(See.only(player), (FreeColGameObject)repair);
}
/**
* Demotes a unit.
*
* @param winner The <code>Unit</code> that won.
* @param loser The <code>Unit</code> that lost and should be demoted.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDemoteUnit(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer loserPlayer = (ServerPlayer) loser.getOwner();
StringTemplate loserNation = loser.getApparentOwnerName();
StringTemplate loserLocation = loser.getLocation()
.getLocationNameFor(loserPlayer);
StringTemplate oldName = loser.getLabel();
String messageId = loser.getType().getId() + ".demoted";
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
StringTemplate winnerNation = winner.getApparentOwnerName();
StringTemplate winnerLocation = winner.getLocation()
.getLocationNameFor(winnerPlayer);
UnitType type = loser.getTypeChange(ChangeType.DEMOTION, loserPlayer);
if (type == null || type == loser.getType()) {
logger.warning("Demotion failed, type="
+ ((type == null) ? "null" : "same type: " + type));
return;
}
loser.setType(type);
cs.addMessage(See.only(winnerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, winner)
.setDefaultId("model.unit.unitDemoted")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerPlayer.getNationName())
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", winnerLocation));
cs.addMessage(See.only(loserPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, loser)
.setDefaultId("model.unit.unitDemoted")
.addStringTemplate("%nation%", loserPlayer.getNationName())
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerNation)
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", loserLocation));
}
/**
* Destroy a colony.
*
* @param attacker The <code>Unit</code> that attacked.
* @param colony The <code>Colony</code> that was attacked.
* @param random A pseudo-random number source.
* @param cs The <code>ChangeSet</code> to update.
*/
private void csDestroyColony(Unit attacker, Colony colony, Random random,
ChangeSet cs) {
Game game = attacker.getGame();
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attacker.getApparentOwnerName();
ServerPlayer colonyPlayer = (ServerPlayer) colony.getOwner();
StringTemplate colonyNation = colonyPlayer.getNationName();
int plunder = colony.getPlunder(attacker, random);
// Handle history and messages before colony destruction.
cs.addHistory(colonyPlayer,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.COLONY_DESTROYED)
.addStringTemplate("%nation%", attackerNation)
.addName("%colony%", colony.getName()));
cs.addMessage(See.only(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.colonyBurning", colony.getTile())
.addName("%colony%", colony.getName())
.addAmount("%amount%", plunder)
.addStringTemplate("%nation%", attackerNation)
.addStringTemplate("%unit%", attacker.getLabel()));
cs.addMessage(See.all().except(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.colonyBurning.other", colonyPlayer)
.addName("%colony%", colony.getName())
.addStringTemplate("%nation%", colonyNation)
.addStringTemplate("%attackerNation%", attackerNation));
// Allocate some plunder.
if (plunder > 0) {
attackerPlayer.modifyGold(plunder);
colonyPlayer.modifyGold(-plunder);
cs.addPartial(See.only(attackerPlayer), attackerPlayer, "gold");
cs.addPartial(See.only(colonyPlayer), colonyPlayer, "gold");
}
// Dispose of the colony and its contents.
csDisposeSettlement(colony, cs);
}
/**
* Destroys an Indian settlement.
*
* @param attacker an <code>Unit</code> value
* @param settlement an <code>IndianSettlement</code> value
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csDestroySettlement(Unit attacker,
IndianSettlement settlement,
Random random, ChangeSet cs) {
Game game = getGame();
Tile tile = settlement.getTile();
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
ServerPlayer nativePlayer = (ServerPlayer) settlement.getOwner();
StringTemplate attackerNation = attackerPlayer.getNationName();
StringTemplate nativeNation = nativePlayer.getNationName();
String settlementName = settlement.getName();
boolean capital = settlement.isCapital();
int plunder = settlement.getPlunder(attacker, random);
// Destroy the settlement, update settlement tiles.
csDisposeSettlement(settlement, cs);
// Make the treasure train if there is treasure.
if (plunder > 0) {
List<UnitType> unitTypes = game.getSpecification()
.getUnitTypesWithAbility(Ability.CARRY_TREASURE);
UnitType type = Utils.getRandomMember(logger, "Choose train",
unitTypes, random);
Unit train = new ServerUnit(game, tile, attackerPlayer, type);
train.setTreasureAmount(plunder);
}
// This is an atrocity.
int atrocities = Player.SCORE_SETTLEMENT_DESTROYED;
if (settlement.getType().getClaimableRadius() > 1) atrocities *= 2;
if (capital) atrocities = (atrocities * 3) / 2;
attackerPlayer.modifyScore(atrocities);
cs.addPartial(See.only(attackerPlayer), attackerPlayer, "score");
// Finish with messages and history.
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.indianTreasure", attacker)
.addName("%settlement%", settlementName)
.addAmount("%amount%", plunder));
cs.addHistory(attackerPlayer,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.DESTROY_SETTLEMENT)
.addStringTemplate("%nation%", nativeNation)
.addName("%settlement%", settlementName));
if (capital) {
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"indianSettlement.capitalBurned", attacker)
.addName("%name%", settlementName)
.addStringTemplate("%nation%", nativeNation));
}
if (nativePlayer.checkForDeath() == IS_DEAD) {
cs.addGlobalHistory(game,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.DESTROY_NATION)
.addStringTemplate("%nation%", attackerNation)
.addStringTemplate("%nativeNation%", nativeNation));
}
cs.addAttribute(See.only(attackerPlayer), "sound",
"sound.event.destroySettlement");
}
/**
* Disposes of a settlement and reassign its tiles.
*
* @param settlement The <code>Settlement</code> under attack.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csDisposeSettlement(Settlement settlement, ChangeSet cs) {
logger.finest("Disposing of " + settlement.getName());
ServerPlayer owner = (ServerPlayer) settlement.getOwner();
HashMap<Settlement, Integer> votes = new HashMap<Settlement, Integer>();
// Try to reassign the tiles
List<Tile> owned = settlement.getOwnedTiles();
Tile centerTile = settlement.getTile();
Settlement centerClaimant = null;
while (!owned.isEmpty()) {
Tile tile = owned.remove(0);
votes.clear();
for (Tile t : tile.getSurroundingTiles(1)) {
// For each lost tile, find any neighbouring
// settlements and give them a shout at claiming the tile.
// Note that settlements now can own tiles outside
// their radius--- if we encounter any of these clean
// them up too.
Settlement s = t.getOwningSettlement();
if (s == null) {
;
} else if (s.isDisposed() || s.getOwner() == null) {
// BR#3375773 found a case where tiles were still
// owned by a settlement that had been previously
// destroyed. Keep things simple and just clear
// the ownership on these. The bug occurred
// because settlement.getOwnedTiles() was missing
// some tiles, which should be fixed, but we can
// be defensive here.
t.setOwningSettlement(null);
} else if (s == settlement) {
// Add this to the tiles to process if its not
// there already.
if (!owned.contains(t)) owned.add(t);
} else if (s.getOwner().canOwnTile(tile)
&& (s.getOwner().isIndian()
|| s.getTile().getDistanceTo(tile) <= s.getRadius())) {
// Weight claimant settlements:
// settlements owned by the same player
// > settlements owned by same type of player
// > other settlements
int value = (s.getOwner() == owner) ? 3
: (s.getOwner().isEuropean() == owner.isEuropean()) ? 2
: 1;
if (votes.get(s) != null) value += votes.get(s).intValue();
votes.put(s, new Integer(value));
}
}
Settlement bestClaimant = null;
int bestClaim = 0;
for (Entry<Settlement, Integer> vote : votes.entrySet()) {
if (vote.getValue().intValue() > bestClaim) {
bestClaimant = vote.getKey();
bestClaim = vote.getValue().intValue();
}
}
if (tile == centerTile) {
centerClaimant = bestClaimant; // Defer until settlement gone
} else {
if (bestClaimant == null) {
tile.changeOwnership(null, null);
} else {
tile.changeOwnership(bestClaimant.getOwner(), bestClaimant);
}
cs.add(See.perhaps().always(owner), tile);
}
}
// Settlement goes away
if (!owner.removeSettlement(settlement)) {
throw new IllegalStateException("Failed to remove settlement: "
+ settlement);
}
if (owner.hasSettlement(settlement)) {
throw new IllegalStateException("Still has settlement: "
+ settlement);
}
cs.addDispose(See.perhaps().always(owner), centerTile, settlement);
// Now the settlement is gone, the center tile can be claimed.
if (centerClaimant != null) {
centerTile.changeOwnership(centerClaimant.getOwner(),
centerClaimant);
}
}
/**
* Evade a normal attack.
*
* @param attacker The attacker <code>Unit</code>.
* @param defender A naval <code>Unit</code> that evades the attacker.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csEvadeAttack(Unit attacker, Unit defender, ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attacker.getApparentOwnerName();
ServerPlayer defenderPlayer = (ServerPlayer) defender.getOwner();
StringTemplate defenderNation = defender.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.enemyShipEvaded", attacker)
.addStringTemplate("%unit%", attacker.getLabel())
.addStringTemplate("%enemyUnit%", defender.getLabel())
.addStringTemplate("%enemyNation%", defenderNation));
cs.addMessage(See.only(defenderPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipEvaded", defender)
.addStringTemplate("%unit%", defender.getLabel())
.addStringTemplate("%enemyUnit%", attacker.getLabel())
.addStringTemplate("%enemyNation%", attackerNation));
}
/**
* Evade a bombardment.
*
* @param settlement The attacker <code>Settlement</code>.
* @param defender A naval <code>Unit</code> that evades the attacker.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csEvadeBombard(Settlement settlement, Unit defender,
ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) settlement.getOwner();
ServerPlayer defenderPlayer = (ServerPlayer) defender.getOwner();
StringTemplate defenderNation = defender.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipEvadedBombardment", settlement)
.addName("%colony%", settlement.getName())
.addStringTemplate("%unit%", defender.getLabel())
.addStringTemplate("%nation%", defenderNation));
cs.addMessage(See.only(defenderPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipEvadedBombardment", defender)
.addName("%colony%", settlement.getName())
.addStringTemplate("%unit%", defender.getLabel())
.addStringTemplate("%nation%", defenderNation));
}
/**
* Loot a ship.
*
* @param winner The winning naval <code>Unit</code>.
* @param loser The losing naval <code>Unit</code>
* @param cs A <code>ChangeSet</code> to update.
*/
private void csLootShip(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
if (loser.getGoodsList().size() > 0 && winner.hasSpaceLeft()) {
List<Goods> capture = new ArrayList<Goods>(loser.getGoodsList());
for (Goods g : capture) g.setLocation(null);
LootSession session = new LootSession(winner, loser);
session.setCapture(capture);
cs.add(See.only(winnerPlayer), ChangeSet.ChangePriority.CHANGE_LATE,
new LootCargoMessage(winner, loser.getId(), capture));
}
loser.getGoodsContainer().removeAll();
loser.setState(Unit.UnitState.ACTIVE);
}
/**
* Unit autoequips but loses equipment.
*
* @param attacker The <code>Unit</code> that attacked.
* @param defender The <code>Unit</code> that defended and loses equipment.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csLoseAutoEquip(Unit attacker, Unit defender, ChangeSet cs) {
ServerPlayer defenderPlayer = (ServerPlayer) defender.getOwner();
StringTemplate defenderNation = defenderPlayer.getNationName();
Settlement settlement = defender.getSettlement();
StringTemplate defenderLocation = defender.getLocation()
.getLocationNameFor(defenderPlayer);
EquipmentType equip = defender
.getBestCombatEquipmentType(defender.getAutomaticEquipment());
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerLocation = attacker.getLocation()
.getLocationNameFor(attackerPlayer);
StringTemplate attackerNation = attacker.getApparentOwnerName();
// Autoequipment is not actually with the unit, it is stored
// in the settlement of the unit. Remove it from there.
for (AbstractGoods goods : equip.getGoodsRequired()) {
settlement.removeGoods(goods);
}
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.unitWinColony", attacker)
.addStringTemplate("%location%", attackerLocation)
.addStringTemplate("%nation%", attackerPlayer.getNationName())
.addStringTemplate("%unit%", attacker.getLabel())
.addStringTemplate("%settlement%", settlement.getLocationNameFor(attackerPlayer))
.addStringTemplate("%enemyNation%", defenderNation)
.addStringTemplate("%enemyUnit%", defender.getLabel()));
cs.addMessage(See.only(defenderPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.unitLoseAutoEquip", defender)
.addStringTemplate("%location%", defenderLocation)
.addStringTemplate("%nation%", defenderNation)
.addStringTemplate("%unit%", defender.getLabel())
.addStringTemplate("%settlement%", settlement.getLocationNameFor(defenderPlayer))
.addStringTemplate("%enemyNation%", attackerNation)
.addStringTemplate("%enemyUnit%", attacker.getLabel()));
}
/**
* Unit drops some equipment.
*
* @param winner The <code>Unit</code> that won.
* @param loser The <code>Unit</code> that lost and loses equipment.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csLoseEquip(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer loserPlayer = (ServerPlayer) loser.getOwner();
StringTemplate loserNation = loserPlayer.getNationName();
StringTemplate loserLocation = loser.getLocation()
.getLocationNameFor(loserPlayer);
StringTemplate oldName = loser.getLabel();
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
StringTemplate winnerNation = winner.getApparentOwnerName();
StringTemplate winnerLocation = winner.getLocation()
.getLocationNameFor(winnerPlayer);
EquipmentType equip
= loser.getBestCombatEquipmentType(loser.getEquipment());
// Remove the equipment, accounting for possible loss of
// mobility due to horses going away.
loser.changeEquipment(equip, -1);
loser.setMovesLeft(Math.min(loser.getMovesLeft(),
loser.getInitialMovesLeft()));
String messageId;
if (loser.getEquipment().isEmpty()) {
messageId = "model.unit.unitDemotedToUnarmed";
loser.setState(Unit.UnitState.ACTIVE);
} else {
messageId = loser.getType().getId() + ".demoted";
}
cs.addMessage(See.only(winnerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, winner)
.setDefaultId("model.unit.unitDemoted")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerPlayer.getNationName())
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", winnerLocation));
cs.addMessage(See.only(loserPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, loser)
.setDefaultId("model.unit.unitDemoted")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerNation)
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", loserLocation));
}
/**
* Damage a building or a ship or steal some goods or gold.
*
* @param attacker The attacking <code>Unit</code>.
* @param colony The <code>Colony</code> to pillage.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csPillageColony(Unit attacker, Colony colony,
Random random, ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) attacker.getOwner();
StringTemplate attackerNation = attacker.getApparentOwnerName();
ServerPlayer colonyPlayer = (ServerPlayer) colony.getOwner();
StringTemplate colonyNation = colonyPlayer.getNationName();
// Collect the damagable buildings, ships, movable goods.
List<Building> buildingList = colony.getBurnableBuildingList();
List<Unit> shipList = colony.getShipList();
List<Goods> goodsList = colony.getLootableGoodsList();
// Pick one, with one extra choice for stealing gold.
int pillage = Utils.randomInt(logger, "Pillage choice", random,
buildingList.size() + shipList.size() + goodsList.size()
+ ((colony.canBePlundered()) ? 1 : 0));
if (pillage < buildingList.size()) {
Building building = buildingList.get(pillage);
csDamageBuilding(building, cs);
cs.addMessage(See.only(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.buildingDamaged", colony)
.add("%building%", building.getNameKey())
.addName("%colony%", colony.getName())
.addStringTemplate("%enemyNation%", attackerNation)
.addStringTemplate("%enemyUnit%", attacker.getLabel()));
} else if (pillage < buildingList.size() + shipList.size()) {
Unit ship = shipList.get(pillage - buildingList.size());
if (ship.getRepairLocation() == null) {
csSinkShipAttack(attacker, ship, cs);
} else {
csDamageShipAttack(attacker, ship, cs);
}
} else if (pillage < buildingList.size() + shipList.size()
+ goodsList.size()) {
Goods goods = goodsList.get(pillage - buildingList.size()
- shipList.size());
goods.setAmount(Math.min(goods.getAmount() / 2, 50));
colony.removeGoods(goods);
if (attacker.canAdd(goods)) attacker.add(goods);
cs.addMessage(See.only(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.goodsStolen", colony, goods)
.addAmount("%amount%", goods.getAmount())
.add("%goods%", goods.getType().getNameKey())
.addName("%colony%", colony.getName())
.addStringTemplate("%enemyNation%", attackerNation)
.addStringTemplate("%enemyUnit%", attacker.getLabel()));
} else {
int plunder = Math.max(1, colony.getPlunder(attacker, random) / 5);
colonyPlayer.modifyGold(-plunder);
attackerPlayer.modifyGold(plunder);
cs.addPartial(See.only(colonyPlayer), colonyPlayer, "gold");
cs.addMessage(See.only(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.indianPlunder", colony)
.addAmount("%amount%", plunder)
.addName("%colony%", colony.getName())
.addStringTemplate("%enemyNation%", attackerNation)
.addStringTemplate("%enemyUnit%", attacker.getLabel()));
}
cs.addMessage(See.all().except(colonyPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.indianRaid", colonyPlayer)
.addStringTemplate("%nation%", attackerNation)
.addName("%colony%", colony.getName())
.addStringTemplate("%colonyNation%", colonyNation));
}
/**
* Damage a building in a colony by downgrading it if possible and
* destroying it otherwise. Workers in a destroyed building are
* automatically assigned to other work locations.
*
* @param building a <code>Building</code> value
* @param cs a <code>ChangeSet</code> value
*/
private void csDamageBuilding(Building building, ChangeSet cs) {
Colony colony = building.getColony();
if (building.getType().getUpgradesFrom() == null) {
// Eject units to any available work location.
unit: for (Unit u : building.getUnitList()) {
for (WorkLocation wl : colony.getAvailableWorkLocations()) {
if (wl == building || !wl.canAdd(u)) continue;
u.setLocation(wl);
continue unit;
}
u.setLocation(colony.getTile());
}
colony.removeBuilding(building);
cs.addDispose(See.only((ServerPlayer) colony.getOwner()), colony, building);
} else if (building.canBeDamaged()) {
building.damage();
}
if (isAI()) {
colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false);
}
}
/**
* Promotes a unit.
*
* @param winner The <code>Unit</code> that won and should be promoted.
* @param loser The <code>Unit</code> that lost.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csPromoteUnit(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
StringTemplate winnerNation = winnerPlayer.getNationName();
StringTemplate oldName = winner.getLabel();
UnitType type = winner.getTypeChange(ChangeType.PROMOTION,
winnerPlayer);
if (type == null || type == winner.getType()) {
logger.warning("Promotion failed, type="
+ ((type == null) ? "null" : "same type: " + type));
return;
}
winner.setType(type);
cs.addMessage(See.only(winnerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.unitPromoted", winner)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", winner.getLabel())
.addStringTemplate("%nation%", winnerNation));
}
/**
* Sinks all ships in a colony.
*
* @param attacker The attacker <code>Unit</code>.
* @param colony The <code>Colony</code> to sink ships in.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSinkColonyShips(Unit attacker, Colony colony, ChangeSet cs) {
List<Unit> units = colony.getTile().getUnitList();
while (!units.isEmpty()) {
Unit unit = units.remove(0);
if (unit.isNaval()) {
csSinkShipAttack(attacker, unit, cs);
}
}
}
/**
* Sinks this ship as result of a normal attack.
*
* @param attacker The attacker <code>Unit</code>.
* @param ship The naval <code>Unit</code> to sink.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSinkShipAttack(Unit attacker, Unit ship, ChangeSet cs) {
ServerPlayer shipPlayer = (ServerPlayer) ship.getOwner();
StringTemplate shipNation = ship.getApparentOwnerName();
Unit attackerUnit = (Unit) attacker;
ServerPlayer attackerPlayer = (ServerPlayer) attackerUnit.getOwner();
StringTemplate attackerNation = attackerUnit.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.enemyShipSunk", attackerUnit)
.addStringTemplate("%unit%", attackerUnit.getLabel())
.addStringTemplate("%enemyUnit%", ship.getLabel())
.addStringTemplate("%enemyNation%", shipNation));
cs.addMessage(See.only(shipPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipSunk", ship.getTile())
.addStringTemplate("%unit%", ship.getLabel())
.addStringTemplate("%enemyUnit%", attackerUnit.getLabel())
.addStringTemplate("%enemyNation%", attackerNation));
csSinkShip(ship, attackerPlayer, cs);
}
/**
* Sinks this ship as result of a bombard.
*
* @param settlement The bombarding <code>Settlement</code>.
* @param ship The naval <code>Unit</code> to sink.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSinkShipBombard(Settlement settlement, Unit ship,
ChangeSet cs) {
ServerPlayer attackerPlayer = (ServerPlayer) settlement.getOwner();
ServerPlayer shipPlayer = (ServerPlayer) ship.getOwner();
StringTemplate shipNation = ship.getApparentOwnerName();
cs.addMessage(See.only(attackerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipSunkByBombardment", settlement)
.addName("%colony%", settlement.getName())
.addStringTemplate("%unit%", ship.getLabel())
.addStringTemplate("%nation%", shipNation));
cs.addMessage(See.only(shipPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
"model.unit.shipSunkByBombardment", ship.getTile())
.addName("%colony%", settlement.getName())
.addStringTemplate("%unit%", ship.getLabel()));
csSinkShip(ship, attackerPlayer, cs);
}
/**
* Sink the ship.
*
* @param ship The naval <code>Unit</code> to sink.
* @param attackerPlayer The <code>ServerPlayer</code> that
* attacked, or null
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSinkShip(Unit ship, ServerPlayer attackerPlayer,
ChangeSet cs) {
ServerPlayer shipPlayer = (ServerPlayer) ship.getOwner();
cs.addDispose(See.perhaps().always(shipPlayer),
ship.getLocation(), ship);
if (attackerPlayer != null) {
cs.addAttribute(See.only(attackerPlayer), "sound",
"sound.event.shipSunk");
}
}
/**
* Slaughter a unit.
*
* @param winner The <code>Unit</code> that is slaughtering.
* @param loser The <code>Unit</code> to slaughter.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSlaughterUnit(Unit winner, Unit loser, ChangeSet cs) {
ServerPlayer winnerPlayer = (ServerPlayer) winner.getOwner();
StringTemplate winnerNation = winner.getApparentOwnerName();
StringTemplate winnerLocation = winner.getLocation()
.getLocationNameFor(winnerPlayer);
ServerPlayer loserPlayer = (ServerPlayer) loser.getOwner();
StringTemplate loserNation = loser.getApparentOwnerName();
StringTemplate loserLocation = loser.getLocation()
.getLocationNameFor(loserPlayer);
String messageId = loser.getType().getId() + ".destroyed";
cs.addMessage(See.only(winnerPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, winner)
.setDefaultId("model.unit.unitSlaughtered")
.addStringTemplate("%nation%", loserNation)
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerPlayer.getNationName())
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", winnerLocation));
cs.addMessage(See.only(loserPlayer),
new ModelMessage(ModelMessage.MessageType.COMBAT_RESULT,
messageId, loser.getTile())
.setDefaultId("model.unit.unitSlaughtered")
.addStringTemplate("%nation%", loserPlayer.getNationName())
.addStringTemplate("%unit%", loser.getLabel())
.addStringTemplate("%enemyNation%", winnerNation)
.addStringTemplate("%enemyUnit%", winner.getLabel())
.addStringTemplate("%location%", loserLocation));
if (loserPlayer.isIndian() && loserPlayer.checkForDeath() == IS_DEAD) {
StringTemplate nativeNation = loserPlayer.getNationName();
cs.addGlobalHistory(getGame(),
new HistoryEvent(getGame().getTurn(),
HistoryEvent.EventType.DESTROY_NATION)
.addStringTemplate("%nation%", winnerNation)
.addStringTemplate("%nativeNation%", nativeNation));
}
// Transfer equipment, do not generate messages for the loser.
EquipmentType equip;
while ((equip = loser.getBestCombatEquipmentType(loser.getEquipment()))
!= null) {
loser.changeEquipment(equip, -loser.getEquipmentCount(equip));
csCaptureEquipment(winner, loser, equip, cs);
}
// Destroy unit.
cs.addDispose(See.perhaps().always(loserPlayer),
loser.getLocation(), loser);
}
/**
* Updates the PlayerExploredTile for each new tile on a supplied list,
* and update a changeset as well.
*
* @param newTiles A list of <code>Tile</code>s to update.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csSeeNewTiles(List<Tile> newTiles, ChangeSet cs) {
for (Tile t : newTiles) {
t.updatePlayerExploredTile(this, false);
cs.add(See.only(this), t);
}
}
/**
* Raises the players tax rate, or handles a goods party.
*
* @param tax The new tax rate.
* @param goods The <code>Goods</code> to use in a goods party.
* @param accepted Whether the tax raise was accepted.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csRaiseTax(int tax, Goods goods, boolean accepted,
ChangeSet cs) {
GoodsType goodsType = goods.getType();
Colony colony = (Colony) goods.getLocation();
int amount = Math.min(goods.getAmount(), GoodsContainer.CARGO_SIZE);
if (accepted) {
csSetTax(tax, cs);
logger.info("Accepted tax raise to: " + tax);
} else if (colony.getGoodsCount(goodsType) < amount) {
// Player has removed the goods from the colony,
// so raise the tax anyway.
final int extraTax = 3; // TODO, magic number
csSetTax(tax + extraTax, cs);
cs.add(See.only(this), ChangePriority.CHANGE_NORMAL,
new MonarchActionMessage(Monarch.MonarchAction.FORCE_TAX,
StringTemplate.template("model.monarch.action.FORCE_TAX")
.addAmount("%amount%", tax + extraTax)));
logger.info("Forced tax raise to: " + (tax + extraTax));
} else { // Tea party
Specification spec = getGame().getSpecification();
colony.getGoodsContainer().saveState();
colony.removeGoods(goodsType, amount);
int arrears = market.getPaidForSale(goodsType)
* spec.getIntegerOption("model.option.arrearsFactor")
.getValue();
Market market = getMarket();
market.setArrears(goodsType, arrears);
Turn turn = getGame().getTurn();
List<Modifier> modifiers
= spec.getModifiers("model.modifier.colonyGoodsParty");
Modifier template;
if (modifiers != null && !modifiers.isEmpty()) {
template = modifiers.get(0);
} else { // @compat 0.9.x
template = new Modifier("model.modifier.colonyGoodsParty",
Specification.COLONY_GOODS_PARTY_SOURCE,
50, Modifier.Type.PERCENTAGE);
template.setIncrement(-2, Modifier.Type.ADDITIVE, turn, turn);
} // end compatibility code
Modifier modifier = Modifier.makeTimedModifier("model.goods.bells",
template, turn);
cs.addFeatureChange(this, colony, modifier, true);
cs.add(See.only(this), colony.getGoodsContainer());
cs.add(See.only(this), market.getMarketData(goodsType));
String messageId = goodsType.getId() + ".destroyed";
if (!Messages.containsKey(messageId)) {
messageId = (colony.isLandLocked())
? "model.monarch.colonyGoodsParty.landLocked"
: "model.monarch.colonyGoodsParty.harbour";
}
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
messageId, this)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount)
.add("%goods%", goodsType.getNameKey()));
cs.addAttribute(See.only(this), "flush", Boolean.TRUE.toString());
logger.info("Goods party at " + colony.getName()
+ " with: " + goods + " arrears: " + arrears);
}
}
/**
* Set the player tax rate.
* If this requires a change to the bells bonuses, we have to update
* the whole player (bah) because we can not yet independently update
* the feature container.
*
* @param tax The new tax rate.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csSetTax(int tax, ChangeSet cs) {
setTax(tax);
if (recalculateBellsBonus()) {
cs.add(See.only(this), this);
} else {
cs.addPartial(See.only(this), this, "tax");
}
}
/**
* Adds mercenaries that the player has accepted.
*
* @param mercs A list of mercenaries.
* @param price The price to be charged for them.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csAddMercenaries(List<AbstractUnit> mercs, int price,
ChangeSet cs) {
if (checkGold(price)) {
createUnits(mercs, getEurope());
cs.add(See.only(this), getEurope());
modifyGold(-price);
cs.addPartial(See.only(this), this, "gold");
} else {
getMonarch().setDispleasure(true);
cs.add(See.only(this), ChangePriority.CHANGE_NORMAL,
new MonarchActionMessage(Monarch.MonarchAction.DISPLEASURE,
StringTemplate.template("model.monarch.action.DISPLEASURE")));
}
}
/**
* Check for a special contact panel for a nation. If not found,
* check for a more general one if allowed.
* Assumes this player is European.
*
* @param other The <code>Player</code> nation to being contacted.
* @return An <code>EventPanel</code> key, or null if none appropriate.
*/
private String getContactKey(ServerPlayer other) {
String key = "EventPanel.MEETING_" + other.getNationNameKey();
if (!Messages.containsKey(key)) {
if (other.isEuropean()) {
key = (hasContactedEuropeans()) ? null
: "EventPanel.MEETING_EUROPEANS";
} else {
key = (hasContactedIndians()) ? null
: "EventPanel.MEETING_NATIVES";
}
}
return key;
}
/**
* Make contact between two nations if necessary.
*
* @param other The other <code>ServerPlayer</code>.
* @param tile The <code>Tile</code> contact is made at.
* @param cs A <code>ChangeSet</code> to update.
* @return The other nation if it is welcoming this nation on first landing.
*/
public ServerPlayer csContact(ServerPlayer other, Tile tile, ChangeSet cs) {
if (hasContacted(other)) return null;
// Must be a first contact!
Game game = getGame();
Turn turn = game.getTurn();
ServerPlayer welcomer = null;
if (isIndian()) {
// Ignore native-to-native contacts.
if (!other.isIndian()) {
String key = other.getContactKey(this);
if (key != null) {
cs.addMessage(See.only(other),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
key, other, this));
}
cs.addHistory(other, new HistoryEvent(turn,
HistoryEvent.EventType.MEET_NATION)
.addStringTemplate("%nation%", getNationName()));
}
} else { // (serverPlayer.isEuropean)
String key = getContactKey(other);
if (key != null) {
cs.addMessage(See.only(this),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
key, this, other));
}
// History event for European players.
cs.addHistory(this, new HistoryEvent(turn,
HistoryEvent.EventType.MEET_NATION)
.addStringTemplate("%nation%", other.getNationName()));
// Extra special meeting on first landing!
if (other.isIndian()
&& !isNewLandNamed()
&& tile != null && tile.getOwner() == other) {
welcomer = other;
}
}
// Now make the contact properly.
csChangeStance(Stance.PEACE, other, true, cs);
setTension(other, new Tension(Tension.TENSION_MIN));
other.setTension(this, new Tension(Tension.TENSION_MIN));
return welcomer;
}
@Override
public String toString() {
return "ServerPlayer[name=" + getName() + ",ID=" + getId()
+ ",conn=" + connection + "]";
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "serverPlayer"
*/
public String getServerXMLElementTagName() {
return "serverPlayer";
}
}