/**
* 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.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Random;
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.Colony;
import net.sf.freecol.common.model.CombatModel;
import net.sf.freecol.common.model.EquipmentType;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
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.HighSeas;
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.LostCityRumour;
import net.sf.freecol.common.model.LostCityRumour.RumourType;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Resource;
import net.sf.freecol.common.model.ResourceType;
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.TileImprovement;
import net.sf.freecol.common.model.TileImprovementType;
import net.sf.freecol.common.model.TileType;
import net.sf.freecol.common.model.TradeRoute.Stop;
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.model.pathfinding.CostDeciders;
import net.sf.freecol.common.networking.NewLandNameMessage;
import net.sf.freecol.common.networking.NewRegionNameMessage;
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;
/**
* Server version of a unit.
*
*/
public class ServerUnit extends Unit implements ServerModelObject {
private static final Logger logger = Logger.getLogger(ServerUnit.class.getName());
// How many `nothing' rumours are there.
private static int rumourNothing = -1;
/**
* Trivial constructor required for all ServerModelObjects.
*/
public ServerUnit(Game game, String id) {
super(game, id);
}
/**
* Creates a new ServerUnit.
*
* @param game The <code>Game</code> in which this unit belongs.
* @param location The <code>Location</code> to place this at.
* @param owner The <code>Player</code> owning this unit.
* @param type The type of the unit.
*/
public ServerUnit(Game game, Location location, Player owner,
UnitType type) {
this(game, location, owner, type, type.getDefaultEquipment());
}
/**
* Creates a new ServerUnit.
*
* @param game The <code>Game</code> in which this unit belongs.
* @param location The <code>Location</code> to place this at.
* @param owner The <code>Player</code> owning this unit.
* @param type The type of the unit.
* @param initialEquipment The list of initial EquimentTypes
*/
public ServerUnit(Game game, Location location, Player owner,
UnitType type, EquipmentType... initialEquipment) {
super(game);
visibleGoodsCount = -1;
if (type.canCarryGoods()) {
goodsContainer = new GoodsContainer(game, this);
}
UnitType newType = type.getTargetType(ChangeType.CREATION, owner);
unitType = (newType == null) ? type : newType;
this.owner = owner;
if (isPerson()) {
nationality = owner.getNationID();
ethnicity = nationality;
}
setLocation(location);
workLeft = -1;
workType = null;
this.movesLeft = getInitialMovesLeft();
hitpoints = unitType.getHitPoints();
for (EquipmentType equipmentType : initialEquipment) {
if (EquipmentType.NO_EQUIPMENT.equals(equipmentType)) {
equipment.clear();
break;
}
equipment.incrementCount(equipmentType, 1);
}
setRole();
setStateUnchecked(state);
owner.setUnit(this);
owner.invalidateCanSeeTiles();
owner.modifyScore(unitType.getScoreValue());
}
/**
* New turn for this unit.
*
* @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("ServerUnit.csNewTurn, for " + toString());
ServerPlayer owner = (ServerPlayer) getOwner();
Specification spec = getSpecification();
Location loc = getLocation();
boolean tileDirty = false;
boolean unitDirty = false;
// Check for experience-promotion.
GoodsType produce;
UnitType learn;
if (loc instanceof WorkLocation
&& (produce = getWorkType()) != null
&& (learn = spec.getExpertForProducing(produce)) != null
&& learn != getType()
&& getType().canBeUpgraded(learn, ChangeType.EXPERIENCE)) {
int maximumExperience = getType().getMaximumExperience();
int maxValue = (100 * maximumExperience) /
getType().getUnitTypeChange(learn).getProbability(ChangeType.EXPERIENCE);
if (maxValue > 0
&& Utils.randomInt(logger, "Experience", random, maxValue)
< Math.min(getExperience(), maximumExperience)) {
StringTemplate oldName = getLabel();
setType(learn);
cs.addMessage(See.only(owner),
new ModelMessage(ModelMessage.MessageType.UNIT_IMPROVED,
"model.unit.experience",
getColony(), this)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", getLabel())
.addName("%colony%", getColony().getName()));
logger.finest("Experience upgrade for unit " + getId()
+ " to " + getType());
unitDirty = true;
}
}
// Attrition
if (loc instanceof Tile && ((Tile) loc).getSettlement() == null) {
int attrition = getAttrition() + 1;
setAttrition(attrition);
if (attrition > getType().getMaximumAttrition()) {
cs.addMessage(See.only(owner),
new ModelMessage(ModelMessage.MessageType.UNIT_LOST,
"model.unit.attrition", this)
.addStringTemplate("%unit%", getLabel()));
cs.addDispose(See.perhaps().always(owner), loc, this);
}
} else {
setAttrition(0);
}
setMovesLeft((isUnderRepair() || isInMission()) ? 0
: getInitialMovesLeft());
if (getWorkLeft() > 0) {
unitDirty = true;
switch (getState()) {
case IMPROVING:
// Has the improvement been completed already? Do nothing.
TileImprovement ti = getWorkImprovement();
if (ti.isComplete()) {
setState(UnitState.ACTIVE);
setWorkLeft(-1);
} else {
// Otherwise do work
int amount = (getType().hasAbility(Ability.EXPERT_PIONEER))
? 2 : 1;
int turns = ti.getTurnsToComplete();
if ((turns -= amount) < 0) turns = 0;
ti.setTurnsToComplete(turns);
setWorkLeft(turns);
}
break;
default:
setWorkLeft(getWorkLeft() - 1);
break;
}
if (loc instanceof HighSeas && getOwner().isREF()) {
// Swift travel to America for the REF
setWorkLeft(0);
}
}
if (getWorkLeft() == 0) tileDirty |= csCompleteWork(random, cs);
if (getState() == UnitState.SKIPPED) {
setState(UnitState.ACTIVE);
unitDirty = true;
}
if (tileDirty) {
cs.add(See.perhaps(), getTile());
} else if (unitDirty) {
cs.add(See.perhaps(), this);
} else {
cs.addPartial(See.only(owner), this, "movesLeft");
}
}
/**
* Complete the work a unit is doing.
*
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
* @return True if the tile under the unit needs an update.
*/
private boolean csCompleteWork(Random random, ChangeSet cs) {
setWorkLeft(-1);
if (getLocation() instanceof HighSeas) {
Europe europe = owner.getEurope();
Map map = getGame().getMap();
if (getDestination() == europe) {
setLocation(europe);
logger.info(toString() + " arrives in Europe");
if (getTradeRoute() != null) {
setMovesLeft(0);
setState(UnitState.ACTIVE);
return false;
}
ServerPlayer owner = (ServerPlayer) getOwner();
setDestination(null);
cs.addMessage(See.only(owner),
new ModelMessage(ModelMessage.MessageType.DEFAULT,
"model.unit.arriveInEurope",
europe, this)
.add("%europe%", europe.getNameKey()));
setState(UnitState.ACTIVE);
cs.add(See.only(owner), europe, owner.getHighSeas());
} else if (getDestination() == map) {
logger.info(toString() + " arrives in America");
setDestination(null);
csMove(getFullEntryLocation().getSafeTile(getOwner(), null),
random, cs);
} else if (getDestination() instanceof Settlement) {
Settlement settlement = (Settlement) getDestination();
// Used to setDestination(null) due to bugs with uncleared
// destinations. In this case though the unit is not yet
// at the destination, and perhaps the bugs are now sorted.
PathNode path = map.findPathToEurope(this,
settlement.getTile(), CostDeciders.serverAvoidIllegal());
Tile entry;
if (path == null) {
entry = getFullEntryLocation();
} else {
entry = path.getLastNode().getTile();
setEntryLocation(entry);
}
csMove(entry, random, cs);
logger.info(toString() + " arrives in America"
+ ", sailing for" + settlement.getName()
+ " from " + entry);
} else {
logger.warning(toString() + " has unsupported destination "
+ getDestination());
}
} else {
switch (getState()) {
case FORTIFYING:
setState(UnitState.FORTIFIED);
break;
case IMPROVING:
csImproveTile(random, cs);
return true;
default:
logger.warning("Unknown work completed, state=" + getState());
setState(UnitState.ACTIVE);
break;
}
}
return false;
}
/**
* Completes a tile improvement.
*
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csImproveTile(Random random, ChangeSet cs) {
Tile tile = getTile();
AbstractGoods deliver = getWorkImprovement().getType().getProduction(tile.getType());
if (deliver != null) { // Deliver goods if any
int amount = deliver.getAmount();
if (getType().hasAbility(Ability.EXPERT_PIONEER)) {
amount *= 2;
}
Settlement settlement = tile.getSettlement();
if (settlement != null
&& (ServerPlayer) settlement.getOwner() == owner) {
amount = (int) settlement.getFeatureContainer()
.applyModifier(amount, Modifier.TILE_TYPE_CHANGE_PRODUCTION, deliver.getType());
settlement.addGoods(deliver.getType(), amount);
} else {
List<Settlement> adjacent = new ArrayList<Settlement>();
int newAmount = amount;
for (Tile t : tile.getSurroundingTiles(2)) {
Settlement ts = t.getSettlement();
if (ts != null && (ServerPlayer)ts.getOwner() == owner) {
adjacent.add(ts);
int modAmount = (int) ts.getFeatureContainer()
.applyModifier(amount, Modifier.TILE_TYPE_CHANGE_PRODUCTION, deliver.getType());
if (modAmount > newAmount) {
newAmount = modAmount;
}
}
}
if (adjacent.size() > 0) {
int deliverPerCity = newAmount / adjacent.size();
for (Settlement s : adjacent) {
s.addGoods(deliver.getType(), deliverPerCity);
}
// Add residue to first adjacent settlement.
adjacent.get(0).addGoods(deliver.getType(),
newAmount % adjacent.size());
}
}
}
// Finish up
TileImprovement ti = getWorkImprovement();
TileType changeType = ti.getChange(tile.getType());
if (changeType != null) {
// Changes like clearing a forest need to be completed,
// whereas for changes like road building the improvement
// is already added and now complete.
tile.setType(changeType);
}
// Does a resource get exposed?
TileImprovementType tileImprovementType = ti.getType();
int exposeResource = tileImprovementType.getExposeResourcePercent();
if (exposeResource > 0 && !tile.hasResource()) {
if (Utils.randomInt(logger, "Expose resource", random, 100)
< exposeResource) {
ResourceType resType = RandomChoice.getWeightedRandom(logger,
"Resource type", random,
tile.getType().getWeightedResources());
int minValue = resType.getMinValue();
int maxValue = resType.getMaxValue();
int value = minValue + ((minValue == maxValue) ? 0
: Utils.randomInt(logger, "Resource quantity",
random, maxValue - minValue + 1));
tile.addResource(new Resource(getGame(), tile, resType, value));
}
}
// Expend equipment
EquipmentType type = ti.getExpendedEquipmentType();
changeEquipment(type, -ti.getExpendedAmount());
for (Unit unit : tile.getUnitList()) {
if (unit.getWorkImprovement() != null
&& unit.getWorkImprovement().getType() == ti.getType()
&& unit.getState() == UnitState.IMPROVING) {
unit.setWorkLeft(-1);
unit.setWorkImprovement(null);
unit.setState(UnitState.ACTIVE);
unit.setMovesLeft(0);
}
}
// TODO: make this more generic, currently assumes tools used
EquipmentType tools = getSpecification()
.getEquipmentType("model.equipment.tools");
if (type == tools && getEquipmentCount(tools) == 0) {
ServerPlayer owner = (ServerPlayer) getOwner();
StringTemplate locName
= getLocation().getLocationNameFor(owner);
String messageId = (getType().getDefaultEquipmentType() == type)
? getType() + ".noMoreTools"
: "model.unit.noMoreTools";
cs.addMessage(See.only(owner),
new ModelMessage(ModelMessage.MessageType.WARNING,
messageId, this)
.addStringTemplate("%unit%", getLabel())
.addStringTemplate("%location%", locName));
}
}
/**
* Repair a unit.
*
* @param cs A <code>ChangeSet</code> to update.
*/
public void csRepairUnit(ChangeSet cs) {
ServerPlayer owner = (ServerPlayer) getOwner();
setHitpoints(getHitpoints() + 1);
if (!isUnderRepair()) {
Location loc = getLocation();
cs.addMessage(See.only(owner),
new ModelMessage("model.unit.unitRepaired",
this, (FreeColGameObject) loc)
.addStringTemplate("%unit%", getLabel())
.addStringTemplate("%repairLocation%",
loc.getLocationNameFor(owner)));
}
cs.addPartial(See.only(owner), this, "hitpoints");
}
/**
* If a unit moves, check if an opposing naval unit slows it down.
* Note that the unit moves are reduced here.
*
* @param newTile The <code>Tile</code> the unit is moving to.
* @param random A pseudo-random number source.
* @return Either an enemy unit that causes a slowdown, or null if none.
*/
private Unit getSlowedBy(Tile newTile, Random random) {
Player player = getOwner();
Game game = getGame();
CombatModel combatModel = game.getCombatModel();
boolean pirate = hasAbility(Ability.PIRACY);
Unit attacker = null;
float attackPower = 0, totalAttackPower = 0;
if (!isNaval() || getMovesLeft() <= 0) return null;
for (Tile tile : newTile.getSurroundingTiles(1)) {
// Ships in settlements do not slow enemy ships, but:
// TODO should a fortress slow a ship?
Player enemy;
if (tile.isLand()
|| tile.getColony() != null
|| tile.getFirstUnit() == null
|| (enemy = tile.getFirstUnit().getOwner()) == player) continue;
for (Unit enemyUnit : tile.getUnitList()) {
if ((pirate || enemyUnit.hasAbility(Ability.PIRACY)
|| (enemyUnit.isOffensiveUnit() && player.atWarWith(enemy)))
&& enemyUnit.isNaval()
&& combatModel.getOffencePower(enemyUnit, this) > attackPower) {
attackPower = combatModel.getOffencePower(enemyUnit, this);
totalAttackPower += attackPower;
attacker = enemyUnit;
}
}
}
if (attacker != null) {
float defencePower = combatModel.getDefencePower(attacker, this);
float totalProbability = totalAttackPower + defencePower;
if (Utils.randomInt(logger, "Slowed", random,
Math.round(totalProbability) + 1) < totalAttackPower) {
int diff = Math.max(0, Math.round(totalAttackPower - defencePower));
int moves = Math.min(9, 3 + diff / 3);
setMovesLeft(getMovesLeft() - moves);
logger.info(getId() + " slowed by " + attacker.getId()
+ " by " + Integer.toString(moves) + " moves.");
} else {
attacker = null;
}
}
return attacker;
}
/**
* Explores a lost city, finding a native burial ground.
*
* @param cs A <code>ChangeSet</code> to add changes to.
*/
private void csNativeBurialGround(ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer) getOwner();
Tile tile = getTile();
Player indianPlayer = tile.getOwner();
cs.add(See.only(serverPlayer),
indianPlayer.modifyTension(serverPlayer,
Tension.Level.HATEFUL.getLimit()));
cs.add(See.only(serverPlayer), indianPlayer);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.burialGround", serverPlayer, this)
.addStringTemplate("%nation%", indianPlayer.getNationName()));
}
/**
* Explore a lost city.
*
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to add changes to.
*/
private void csExploreLostCityRumour(Random random, ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer) getOwner();
Tile tile = getTile();
LostCityRumour lostCity = tile.getLostCityRumour();
if (lostCity == null) return;
Game game = getGame();
Specification spec = game.getSpecification();
int difficulty = spec.getInteger("model.option.rumourDifficulty");
int dx = 10 - difficulty;
UnitType unitType;
Unit newUnit = null;
List<UnitType> treasureUnitTypes
= spec.getUnitTypesWithAbility(Ability.CARRY_TREASURE);
RumourType rumour = lostCity.getType();
if (rumour == null) {
rumour = lostCity.chooseType(this, difficulty, random);
}
// Filter out failing cases that could only occur if the
// type was explicitly set in debug mode.
switch (rumour) {
case BURIAL_GROUND: case MOUNDS:
if (tile.getOwner() == null || !tile.getOwner().isIndian()) {
rumour = RumourType.NOTHING;
}
break;
case LEARN:
if (getType().getUnitTypesLearntInLostCity().isEmpty()) {
rumour = RumourType.NOTHING;
}
break;
default:
break;
}
// Mounds are a special case that degrade to other cases.
boolean mounds = rumour == RumourType.MOUNDS;
if (mounds) {
boolean done = false;
boolean burial = false;
while (!done) {
rumour = lostCity.chooseType(this, difficulty, random);
switch (rumour) {
case EXPEDITION_VANISHES: case NOTHING: case TRIBAL_CHIEF:
case RUINS:
done = true;
break;
case BURIAL_GROUND:
if (tile.getOwner() != null && tile.getOwner().isIndian()
&& !burial) {
csNativeBurialGround(cs);
burial = true;
}
break;
default:
; // unacceptable result for mounds
}
}
}
logger.info("Unit " + getId() + " is exploring rumour " + rumour);
switch (rumour) {
case BURIAL_GROUND:
csNativeBurialGround(cs);
break;
case EXPEDITION_VANISHES:
cs.addDispose(See.perhaps().always(serverPlayer), tile, this);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.expeditionVanishes", serverPlayer));
break;
case NOTHING:
if (game.getTurn().getYear() % 100 == 12
&& Utils.randomInt(logger, "Mayans?", random, 4) == 0) {
int years = 2012 - game.getTurn().getYear();
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.mayans", serverPlayer, this)
.add("%years%", Integer.toString(years)));
break;
} else if (mounds) {
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.moundsNothing", serverPlayer, this));
break;
}
if (rumourNothing < 0) {
int i;
for (i = 0; Messages.containsKey("lostCityRumour.nothing."
+ Integer.toString(i)); i++);
rumourNothing = i;
}
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.nothing."
+ Integer.toString(Utils.randomInt(logger,
"Nothing rumour", random, rumourNothing)),
serverPlayer, this));
break;
case LEARN:
StringTemplate oldName = getLabel();
List<UnitType> learnTypes = getType().getUnitTypesLearntInLostCity();
unitType = Utils.getRandomMember(logger, "Choose learn",
learnTypes, random);
setType(unitType);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.learn", serverPlayer, this)
.addStringTemplate("%unit%", oldName)
.add("%type%", getType().getNameKey()));
break;
case TRIBAL_CHIEF:
int chiefAmount = Utils.randomInt(logger, "Chief base amount",
random, dx * 10) + dx * 5;
serverPlayer.modifyGold(chiefAmount);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold", "score");
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
((mounds) ? "lostCityRumour.moundsTrinkets"
: "lostCityRumour.tribalChief"),
serverPlayer, this)
.addAmount("%money%", chiefAmount));
break;
case COLONIST:
List<UnitType> foundTypes = spec.getUnitTypesWithAbility("model.ability.foundInLostCity");
unitType = Utils.getRandomMember(logger, "Choose found",
foundTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer, unitType);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.colonist", serverPlayer, newUnit));
break;
case CIBOLA:
String cityName = game.getCityOfCibola();
if (cityName != null) {
int treasureAmount = Utils.randomInt(logger,
"Base treasure amount", random, dx * 600) + dx * 300;
unitType = Utils.getRandomMember(logger, "Choose train",
treasureUnitTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer, unitType);
newUnit.setTreasureAmount(treasureAmount);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.cibola", serverPlayer, newUnit)
.add("%city%", cityName)
.addAmount("%money%", treasureAmount));
cs.addGlobalHistory(game,
new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.CITY_OF_GOLD)
.addStringTemplate("%nation%", serverPlayer.getNationName())
.add("%city%", cityName)
.addAmount("%treasure%", treasureAmount));
break;
}
// Fall through, found all the cities of gold.
case RUINS:
int ruinsAmount = Utils.randomInt(logger,
"Base ruins amount", random, dx * 2) * 300 + 50;
if (ruinsAmount < 500) { // TODO remove magic number
serverPlayer.modifyGold(ruinsAmount);
cs.addPartial(See.only(serverPlayer), serverPlayer,
"gold", "score");
} else {
unitType = Utils.getRandomMember(logger, "Choose train",
treasureUnitTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer, unitType);
newUnit.setTreasureAmount(ruinsAmount);
}
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
((mounds) ? "lostCityRumour.moundsTreasure"
: "lostCityRumour.ruins"),
serverPlayer, ((newUnit != null) ? newUnit
: this))
.addAmount("%money%", ruinsAmount));
break;
case FOUNTAIN_OF_YOUTH:
Europe europe = serverPlayer.getEurope();
if (europe == null) {
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.fountainOfYouthWithoutEurope",
serverPlayer, this));
} else {
if (serverPlayer.hasAbility("model.ability.selectRecruit")
&& !serverPlayer.isAI()) { // TODO: let the AI select
// Remember, and ask player to select
serverPlayer.setRemainingEmigrants(dx);
cs.addTrivial(See.only(serverPlayer), "fountainOfYouth",
ChangeSet.ChangePriority.CHANGE_LATE,
"migrants", Integer.toString(dx));
} else {
List<RandomChoice<UnitType>> recruitables
= serverPlayer.generateRecruitablesList();
for (int k = 0; k < dx; k++) {
UnitType type = RandomChoice
.getWeightedRandom(logger,
"Choose FoY", random, recruitables);
new ServerUnit(game, europe, serverPlayer, type);
}
cs.add(See.only(serverPlayer), europe);
}
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
"lostCityRumour.fountainOfYouth", serverPlayer, this));
}
cs.addAttribute(See.only(serverPlayer),
"sound", "sound.event.fountainOfYouth");
break;
case NO_SUCH_RUMOUR: case MOUNDS:
default:
logger.warning("Bogus rumour type: " + rumour);
break;
}
tile.removeLostCityRumour();
}
/**
* Activate sentried units on a tile.
*
* @param tile The <code>Tile</code> to activate sentries on.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csActivateSentries(Tile tile, ChangeSet cs) {
for (Unit u : tile.getUnitList()) {
if (u.getState() == UnitState.SENTRY) {
u.setState(UnitState.ACTIVE);
cs.add(See.perhaps(), u);
}
}
}
/**
* Collects the tiles surrounding this unit that the player
* can not currently see, but now should as a result of a move.
*
* @param tile The center tile to look from.
* @return A list of new tiles to see.
*/
public List<Tile> collectNewTiles(Tile tile) {
List<Tile> newTiles = new ArrayList<Tile>();
int los = getLineOfSight();
for (Tile t : tile.getSurroundingTiles(los)) {
if (!getOwner().canSee(t)) newTiles.add(t);
}
return newTiles;
}
/**
* Move a unit.
*
* @param newTile The <code>Tile</code> to move to.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csMove(Tile newTile, Random random, ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer) getOwner();
Game game = getGame();
Turn turn = game.getTurn();
// Plan to update tiles that could not be seen before but will
// now be within the line-of-sight.
List<Tile> newTiles = collectNewTiles(newTile);
// Update unit state.
Location oldLocation = getLocation();
setState(UnitState.ACTIVE);
setStateToAllChildren(UnitState.SENTRY);
if (oldLocation instanceof HighSeas) {
; // Do not try to calculate move cost from Europe!
} else if (oldLocation instanceof Unit) {
setMovesLeft(0); // Disembark always consumes all moves.
} else {
if (getMoveCost(newTile) <= 0) {
logger.warning("Move of unit: " + getId()
+ " from: " + ((oldLocation == null) ? "null"
: oldLocation.getTile().getId())
+ " to: " + newTile.getId()
+ " has bogus cost: " + getMoveCost(newTile));
setMovesLeft(0);
}
setMovesLeft(getMovesLeft() - getMoveCost(newTile));
}
// Do the move and explore a rumour if needed.
setLocation(newTile);
if (newTile.hasLostCityRumour() && serverPlayer.isEuropean()) {
csExploreLostCityRumour(random, cs);
}
// Unless moving in from off-map, update the old location and
// make sure the move is always visible even if the unit
// dies (including the animation). However, dead units
// make no discoveries. Always update the new tile.
if (oldLocation instanceof Tile) {
cs.addMove(See.perhaps().always(serverPlayer), this,
oldLocation, newTile);
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) oldLocation);
} else {
cs.add(See.only(serverPlayer), (FreeColGameObject) oldLocation);
}
cs.add(See.perhaps().always(serverPlayer), newTile);
if (isDisposed()) return;
serverPlayer.csSeeNewTiles(newTiles, cs);
if (newTile.isLand()) {
// Claim land for tribe?
Settlement settlement;
if (newTile.getOwner() == null
&& serverPlayer.isIndian()
&& (settlement = getIndianSettlement()) != null
&& (newTile.getPosition()
.getDistance(settlement.getTile().getPosition())
< settlement.getRadius()
+ settlement.getType().getExtraClaimableRadius())) {
newTile.setOwner(serverPlayer);
}
// Check for first landing
String newLand = null;
if (serverPlayer.isEuropean()
&& !serverPlayer.isNewLandNamed()) {
newLand = Messages.getNewLandName(serverPlayer);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.DEFAULT,
"EventPanel.FIRST_LANDING", serverPlayer));
// Set the default value now to prevent multiple attempts.
// The user setNewLandName can override.
serverPlayer.setNewLandName(newLand);
}
// Check for new contacts.
ServerPlayer welcomer = null;
for (Tile t : newTile.getSurroundingTiles(1, 1)) {
if (t == null || !t.isLand()) {
continue; // Invalid tile for contact
}
settlement = t.getSettlement();
ServerPlayer other = (settlement != null)
? (ServerPlayer) t.getSettlement().getOwner()
: (t.getFirstUnit() != null)
? (ServerPlayer) t.getFirstUnit().getOwner()
: null;
if (other == null
|| other == serverPlayer) continue; // No contact
if (serverPlayer.csContact(other, newTile, cs) != null) {
welcomer = other;
}
// Initialize alarm for native settlements.
if (settlement instanceof IndianSettlement) {
IndianSettlement is = (IndianSettlement) settlement;
if (!is.hasContactedSettlement(serverPlayer)) {
is.makeContactSettlement(serverPlayer);
cs.add(See.only(serverPlayer), is);
}
}
csActivateSentries(t, cs);
}
if (newLand != null) {
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new NewLandNameMessage(this, newLand, welcomer,
((welcomer == null) ? -1
: welcomer.getNumberOfSettlements()), false));
}
} else { // water
for (Tile t : newTile.getSurroundingTiles(1, 1)) {
if (t == null || t.isLand() || t.getFirstUnit() == null) {
continue;
}
if ((ServerPlayer) t.getFirstUnit().getOwner()
!= serverPlayer) csActivateSentries(t, cs);
}
}
// Check for slowing units.
Unit slowedBy = getSlowedBy(newTile, random);
if (slowedBy != null) {
StringTemplate enemy = slowedBy.getApparentOwnerName();
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.unit.slowed", this, slowedBy)
.addStringTemplate("%unit%", Messages.getLabel(this))
.addStringTemplate("%enemyUnit%", Messages.getLabel(slowedBy))
.addStringTemplate("%enemyNation%", enemy));
}
// Check for region discovery
Region region = newTile.getDiscoverableRegion();
if (serverPlayer.isEuropean() && region != null) {
if (region.isPacific()) {
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.DEFAULT,
"EventPanel.DISCOVER_PACIFIC", serverPlayer));
cs.addRegion(serverPlayer, region,
Messages.message("model.region.pacific"));
} else if (region.getName() == null) {
// Really newly discovered.
String defaultName = Messages.getDefaultRegionName(serverPlayer,
region.getType());
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new NewRegionNameMessage(region, newTile, defaultName));
// Set the default name to prevent multiple attempts.
region.setName(defaultName);
}
}
}
/**
* Remove equipment from a unit.
*
* @param settlement The <code>Settlement</code> where the unit is
* (may be null if the unit is in Europe).
* @param remove A collection of <code>EquipmentType</code> to remove.
* @param amount Override the amount of equipment to remove.
* @param random A pseudo-random number source.
* @param cs A <code>ChangeSet</code> to update.
*/
public void csRemoveEquipment(Settlement settlement,
Collection<EquipmentType> remove,
int amount, Random random, ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer) getOwner();
for (EquipmentType e : remove) {
int a = (amount > 0) ? amount : getEquipmentCount(e);
for (AbstractGoods goods : e.getGoodsRequired()) {
GoodsType goodsType = goods.getType();
int n = goods.getAmount() * a;
if (isInEurope()) {
if (serverPlayer.canTrade(goodsType,
Market.Access.EUROPE)) {
serverPlayer.sell(null, goodsType, n, random);
serverPlayer.csFlushMarket(goodsType, cs);
}
} else if (settlement != null) {
settlement.addGoods(goodsType, n);
}
}
// Removals can not cause incompatible-equipment trouble
changeEquipment(e, -a);
}
}
/**
* Is there work for a unit to do at a stop?
*
* @param stop The <code>Stop</code> to test.
* @return True if the unit should load or unload cargo at the stop.
*/
public boolean hasWorkAtStop(Stop stop) {
List<GoodsType> stopGoods = stop.getCargo();
int cargoSize = stopGoods.size();
for (Goods goods : getGoodsList()) {
GoodsType type = goods.getType();
if (stopGoods.contains(type)) {
if (getLoadableAmount(type) > 0) {
// There is space on the unit to load some more
// of this goods type, so return true if there is
// some available at the stop.
Location loc = stop.getLocation();
if (loc instanceof Colony) {
if (((Colony) loc).getExportAmount(type) > 0) {
return true;
}
} else if (loc instanceof Europe) {
return true;
}
} else {
cargoSize--; // No room for more of this type.
}
} else {
return true; // This type should be unloaded here.
}
}
// Return true if there is space left, and something to load.
return getSpaceLeft() > 0 && cargoSize > 0;
}
/**
* Returns the tag name of the root element representing this object.
*
* @return "serverUnit"
*/
public String getServerXMLElementTagName() {
return "serverUnit";
}
}