/**
* 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.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.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.FeatureContainer;
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;
// 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;
featureContainer = new FeatureContainer();
europe = null;
if (nation != null && nation.getType() != null) {
this.nationType = nation.getType();
this.nationID = nation.getId();
try {
featureContainer.add(nationType.getFeatureContainer());
} 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 True if this player should die.
*/
public boolean checkForDeath() {
/*
* 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();
case COLONIAL: // Handle the hard case below
if (isUnknownEnemy()) return false;
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.isConnected()) return false;
}
return true;
case ROYAL:
return getRebels().isEmpty();
case UNDEAD:
return getUnits().isEmpty();
default:
throw new IllegalStateException("Bogus player type");
}
// Quick check for a colony
if (!getColonies().isEmpty()) {
return false;
}
// Verify player units
boolean hasCarrier = false;
List<Unit> unitList = getUnits();
for(Unit unit : unitList){
boolean isValidUnit = false;
if(unit.isCarrier()){
hasCarrier = true;
continue;
}
// Can found new colony
if(unit.isColonist()){
isValidUnit = true;
}
// Can capture units
if(unit.isOffensiveUnit()){
isValidUnit = true;
}
if(!isValidUnit){
continue;
}
// Verify if unit is in new world
Location unitLocation = unit.getLocation();
// unit in new world
if(unitLocation instanceof Tile){
logger.info(getName() + " found colonist in new world");
return false;
}
// onboard a carrier
if(unit.isOnCarrier()){
Unit carrier = (Unit) unitLocation;
// carrier in new world
if(carrier.getLocation() instanceof Tile){
logger.info(getName() + " found colonist aboard carrier in new world");
return false;
}
}
}
/*
* At this point we know the player does not have any valid units or
* settlements on the map.
*/
// After the season cutover year, no presence in New World
// means death
int mandatory = getGame().getSpecification().getInteger("model.option.mandatoryColonyYear");
if (getGame().getTurn().getYear() >= mandatory) {
logger.info(getName() + " no presence in new world after "
+ mandatory);
return true;
}
int goldNeeded = 0;
/*
* No carrier, check if has gold to buy one
*/
if(!hasCarrier){
/*
* Find the cheapest naval unit
*/
Iterator<UnitType> navalUnits = getSpecification()
.getUnitTypesWithAbility(Ability.NAVAL_UNIT).iterator();
int lowerPrice = Integer.MAX_VALUE;
while(navalUnits.hasNext()){
UnitType unit = navalUnits.next();
int unitPrice = getEurope().getUnitPrice(unit);
// cannot be bought
if(unitPrice == UnitType.UNDEFINED){
continue;
}
if(unitPrice < lowerPrice){
lowerPrice = unitPrice;
}
}
//Sanitation
if(lowerPrice == Integer.MAX_VALUE){
logger.warning(getName() + " could not find naval unit to buy");
return true;
}
goldNeeded += lowerPrice;
// cannot buy carrier
if (!checkGold(goldNeeded)) {
logger.info(getName() + " does not have enough money to buy carrier");
return true;
}
logger.info(getName() + " has enough money to buy carrier, has="
+ getGold() + ", needs=" + lowerPrice);
}
/*
* Check if player has colonists.
* We already checked that it has (or can buy) a carrier to
* transport them to New World
*/
List<Unit> units = new ArrayList<Unit>();
units.addAll(getEurope().getUnitList());
units.addAll(getHighSeas().getUnitList());
for (Unit eu : units) {
if (eu.isCarrier()) {
/*
* The carrier has colonist units on board
*/
for (Unit u : eu.getUnitList()) {
if (u.isColonist()) return false;
}
// The carrier has units or goods that can be sold.
if (eu.getGoodsCount() > 0) {
logger.info(getName() + " has goods to sell");
return false;
}
continue;
}
if (eu.isColonist()) {
logger.info(getName() + " has colonist unit waiting in port");
return false;
}
}
// No colonists, check if has gold to train or recruit one.
int goldToRecruit = getEurope().getRecruitPrice();
/*
* Find the cheapest colonist, either by recruiting or training
*/
Iterator<UnitType> trainedUnits = getSpecification().getUnitTypesTrainedInEurope().iterator();
int goldToTrain = Integer.MAX_VALUE;
while(trainedUnits.hasNext()){
UnitType unit = trainedUnits.next();
if(!unit.hasAbility("model.ability.foundColony")){
continue;
}
int unitPrice = getEurope().getUnitPrice(unit);
// cannot be bought
if(unitPrice == UnitType.UNDEFINED){
continue;
}
if(unitPrice < goldToTrain){
goldToTrain = unitPrice;
}
}
goldNeeded += Math.min(goldToTrain, goldToRecruit);
if (checkGold(goldNeeded)) return false;
// Does not have enough money for recruiting or training
logger.info(getName() + " does not have enough money for recruiting or training");
return true;
}
/**
* 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)
.addName("%settlement%", settlement.getNameFor(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>>();
FeatureContainer fc = getFeatureContainer();
for (UnitType unitType : getSpecification().getUnitTypeList()) {
if (unitType.isRecruitable()
&& fc.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);
}
/**
* 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, so the units are created in Europe.
*
* @param abstractUnits The list of <code>AbstractUnit</code>s to create.
* @return A list of units created.
*/
public List<Unit> createUnits(List<AbstractUnit> abstractUnits) {
Game game = getGame();
Specification spec = game.getSpecification();
List<Unit> units = new ArrayList<Unit>();
Europe europe = getEurope();
if (europe == null) return units;
for (AbstractUnit au : abstractUnits) {
for (int i = 0; i < au.getNumber(); i++) {
units.add(new ServerUnit(game, europe, this,
au.getUnitType(spec), au.getEquipment(spec)));
}
}
return units;
}
/**
* 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;
}
/**
* Makes the entire map visible.
* Debug mode helper.
*/
public void revealMap() {
for (Tile tile: getGame().getMap().getAllTiles()) {
setExplored(tile);
}
getSpecification().getBooleanOption(GameOptions.FOG_OF_WAR)
.setValue(false);
invalidateCanSeeTiles();
}
/**
* 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) getFeatureContainer()
.applyModifier(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) getFeatureContainer()
.applyModifier(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");
}
// 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()));
}
}
}
}
/**
* 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.getFeatureContainer()
.applyModifier(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());
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();
FeatureContainer fc = getFeatureContainer();
for (int i = 0; i < Europe.RECRUIT_COUNT; i++) {
if (!fc.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;
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 only if this was an ordinary
// migration where we did not select the unit type.
// Other cases were selected.
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.
FeatureContainer fc = colony.getFeatureContainer();
for (Modifier m : fc.getModifiers()) {
if ("model.modifier.colonyGoodsParty".equals(m.getSource())) {
fc.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 units aboard
Unit u;
while ((u = ship.getFirstUnit()) != null) {
u.setLocation(null);
cs.addDispose(See.only(player), null, u); // Only owner-visible
}
// Damage the ship and send it off for repair
ship.getGoodsContainer().removeAll();
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 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()) {
cs.addGlobalHistory(game,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.DESTROY_NATION)
.addStringTemplate("%nation%", 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.
*/
private void csDisposeSettlement(Settlement settlement, ChangeSet cs) {
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
cs.addDispose(See.perhaps().always(owner), centerTile, settlement);
if (centerClaimant != null) {
centerTile.changeOwnership(centerClaimant.getOwner(),
centerClaimant);
}
owner.removeSettlement(settlement);
}
/**
* 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.getSpaceLeft() > 0) {
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())
.addName("%settlement%", settlement.getNameFor(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())
.addName("%settlement%", settlement.getNameFor(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);
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()));
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(colonyPlayer), colony, building);
} else if (building.canBeDamaged()) {
building.damage();
}
if (isAI()) {
colony.firePropertyChange(Colony.REARRANGE_WORKERS, true, false);
}
} 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.getSpaceLeft() > 0) 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));
}
/**
* 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.
* @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);
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()) {
StringTemplate nativeNation = loserPlayer.getNationName();
cs.addGlobalHistory(getGame(),
new HistoryEvent(getGame().getTurn(),
HistoryEvent.EventType.DESTROY_NATION)
.addStringTemplate("%nation%", 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
colony.getFeatureContainer()
.addModifier(Modifier.makeTimedModifier("model.goods.bells",
template, turn));
// Have to update the colony to pick up the feature container.
// Otherwise we could get away with just sending the goods
// container. TODO: make the feature container updateable?
cs.add(See.only(this), colony);
// cs.add(See.only(serverPlayer), 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())
.addName("%amount%", Integer.toString(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);
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";
}
}