freecol/src/net/sf/freecol/server/model/ServerUnit.java

1095 lines
46 KiB
Java

/**
* Copyright (C) 2002-2017 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.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.logging.Logger;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.i18n.NameCache;
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.Europe;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
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.ModelMessage;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Stance;
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.Role;
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.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.UnitChangeType;
import net.sf.freecol.common.model.UnitTypeChange;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.WorkLocation;
import net.sf.freecol.common.networking.ChangeSet;
import net.sf.freecol.common.networking.ChangeSet.See;
import net.sf.freecol.common.networking.FountainOfYouthMessage;
import net.sf.freecol.common.networking.Message;
import net.sf.freecol.common.networking.NewLandNameMessage;
import net.sf.freecol.common.networking.NewRegionNameMessage;
import net.sf.freecol.common.option.GameOptions;
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.RandomChoice;
import static net.sf.freecol.common.util.CollectionUtils.*;
import static net.sf.freecol.common.util.RandomUtils.*;
/**
* Server version of a unit.
*/
public class ServerUnit extends Unit implements TurnTaker {
private static final Logger logger = Logger.getLogger(ServerUnit.class.getName());
/**
* Trivial constructor for Game.newInstance.
*
* @param game The {@code Game} in which this unit belongs.
* @param id The object identifier.
*/
public ServerUnit(Game game, String id) {
super(game, id);
}
/**
* Creates a new ServerUnit.
*
* -vis: Visibility issues depending on location.
* -til: Changes appearance if unit goes into a colony.
*
* @param game The {@code Game} in which this unit belongs.
* @param location The {@code Location} to place this at.
* @param owner The {@code Player} 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.getDefaultRole());
}
/**
* Create a new ServerUnit from a template.
*
* Note all FCGOTs are looked up in the specification by id,
* allowing the template to derive from a different specification
* as might happen when loading a scenario map.
*
* -vis: Visibility issues depending on location.
* -til: Changes appearance if unit goes into a colony.
*
* @param game The {@code Game} in which this unit belongs.
* @param location The {@code Location} to place this at.
* @param template A {@code Unit} to copy from.
*/
public ServerUnit(Game game, Location location, Unit template) {
this(game, location,
game.getPlayerByNationId(template.getOwner().getNationId()),
game.getSpecification().getUnitType(template.getType().getId()),
game.getSpecification().getDefaultRole());
final Specification spec = getSpecification();
if (template.getName() != null) setName(template.getName());
setNationality(template.getNationality());
setEthnicity(template.getEthnicity());
workLeft = template.getWorkLeft();
workType = spec.getGoodsType(template.getWorkType().getId());
movesLeft = template.getMovesLeft();
hitPoints = template.getType().getHitPoints();
changeRole(spec.getRole(template.getRole().getId()),
template.getRoleCount());
setStateUnchecked(template.getState());
if (getType().canCarryGoods()) {
setGoodsContainer(new GoodsContainer(game, this));
}
this.visibleGoodsCount = -1;
}
/**
* Creates a new ServerUnit.
*
* -vis: Visibility issues depending on location.
* -til: Changes appearance if unit goes into a colony.
*
* @param game The {@code Game} in which this unit belongs.
* @param location The {@code Location} to place this at.
* @param owner The {@code Player} owning this unit.
* @param type The type of the unit.
* @param role The role of the unit.
*/
public ServerUnit(Game game, Location location, Player owner,
UnitType type, Role role) {
super(game);
final Specification spec = getSpecification();
this.owner = owner;
this.type = type;
this.state = UnitState.ACTIVE; // placeholder
this.role = getSpecification().getDefaultRole(); // placeholder
this.location = null;
this.entryLocation = null;
this.workLeft = -1;
this.workType = null;
this.movesLeft = getInitialMovesLeft();
this.experienceType = null;
this.experience = 0;
this.workImprovement = null;
this.student = this.teacher = null;
this.turnsOfTraining = 0;
this.indianSettlement = null;
this.destination = null;
this.tradeRoute = null;
this.currentStop = -1;
this.treasureAmount = 0;
this.attrition = 0;
this.visibleGoodsCount = -1;
// Check for creation change
UnitTypeChange uc = getUnitChange(UnitChangeType.CREATION);
if (uc != null) this.type = uc.to;
if (this.type.hasAbility(Ability.PERSON)) {
this.nationality = owner.getNationId();
this.ethnicity = nationality;
} else {
this.nationality = null;
this.ethnicity = null;
}
this.hitPoints = this.type.getHitPoints();
// Fix up role, state and location now other values are present.
changeRole(role, role.getMaximumCount());
setStateUnchecked(state);
setLocation(location);//-vis(owner),-til
if (getType().canCarryGoods()) {
setGoodsContainer(new GoodsContainer(game, this));
}
this.owner.addUnit(this);
}
/**
* Completes a tile improvement.
*
* +til: Resolves the change of appearance.
*
* @param random A pseudo-random number source.
* @param cs A {@code ChangeSet} to update.
*/
private void csImproveTile(Random random, ChangeSet cs) {
Tile tile = getTile();
tile.cacheUnseen();//+til
AbstractGoods deliver = getWorkImprovement().getType()
.getProduction(tile.getType());
if (deliver != null) { // Deliver goods if any
final Turn turn = getGame().getTurn();
int amount = deliver.getAmount();
amount = (int)this.applyModifiers(amount, turn,
Modifier.TILE_TYPE_CHANGE_PRODUCTION, deliver.getType());
Settlement settlement = tile.getOwningSettlement();
if (settlement != null && owner.owns(settlement)) {
amount = (int)settlement.applyModifiers(amount, turn,
Modifier.TILE_TYPE_CHANGE_PRODUCTION, deliver.getType());
settlement.addGoods(deliver.getType(), amount);
}
}
// 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.changeType(changeType);//-til
}
// Does a resource get exposed?
TileImprovementType tileImprovementType = ti.getType();
int exposeResource = tileImprovementType.getExposeResourcePercent();
if (exposeResource > 0 && !tile.hasResource()) {
if (randomInt(logger, "Expose resource", random, 100)
< exposeResource) {
ResourceType resType = RandomChoice
.getWeightedRandom(logger, "Resource type",
tile.getType().getResourceTypes(),
random);
int minValue = resType.getMinValue();
int maxValue = resType.getMaxValue();
int value = minValue + ((minValue == maxValue) ? 0
: randomInt(logger, "Resource quantity",
random, maxValue - minValue + 1));
tile.addResource(new Resource(getGame(), tile,
resType, value));//-til
}
}
// Expend equipment.
if (changeRoleCount(-ti.getType().getExpendedAmount())) {
// FIXME: assumes tools, make more generic, use
// ti.getType().getRequiredRole().getRequiredGoods()
ServerPlayer owner = (ServerPlayer)getOwner();
StringTemplate locName
= getLocation().getLocationLabelFor(owner);
String messageId = getType() + ".noMoreTools";
if (!Messages.containsKey(messageId)) {
messageId = "model.unit.noMoreTools";
}
cs.addMessage(owner,
new ModelMessage(ModelMessage.MessageType.WARNING,
messageId, this)
.addStringTemplate("%unit%", getLabel())
.addStringTemplate("%location%", locName));
}
// Cancel other co-located improvements of the same type
for (Unit unit : transform(tile.getUnits(),
u -> (u.getWorkImprovement() != null
&& u.getWorkImprovement().getType() == ti.getType()
&& u.getState() == UnitState.IMPROVING))) {
unit.setWorkLeft(-1);
unit.setWorkImprovement(null);
unit.setState(UnitState.ACTIVE);
unit.setMovesLeft(0);
}
}
/**
* Embark a unit.
*
* @param carrier The {@code Unit} to embark on.
* @param cs A {@code ChangeSet} to update.
*/
public void csEmbark(Unit carrier, ChangeSet cs) {
final ServerPlayer owner = (ServerPlayer)getOwner();
Location oldLocation = getLocation();
Colony colony = (oldLocation instanceof WorkLocation) ? getColony()
: null;
if (colony != null) oldLocation.getTile().cacheUnseen();//+til
setLocation(carrier);//-vis: only if on a different tile
//-til if moving from colony
setMovesLeft(0);
cs.add(See.only(owner), (colony != null) ? colony
: (FreeColGameObject)oldLocation);
if (carrier.getLocation() != oldLocation) {
cs.add(See.only(owner), carrier);
}
if (oldLocation instanceof Tile) {
if (carrier.getTile() != oldLocation) {
cs.addMove(See.only(owner), this, oldLocation,
carrier.getTile());
owner.invalidateCanSeeTiles();//+vis(serverPlayer)
}
cs.addDisappear(owner, (Tile)oldLocation, this);
}
}
/**
* Repair a unit.
*
* @param cs A {@code ChangeSet} to update.
*/
public void csRepairUnit(ChangeSet cs) {
ServerPlayer owner = (ServerPlayer) getOwner();
setHitPoints(getHitPoints() + 1);
if (!isDamaged()) {
Location loc = getLocation();
cs.addMessage(owner,
new ModelMessage(ModelMessage.MessageType.UNIT_REPAIRED,
"model.unit.unitRepaired",
this, (FreeColGameObject)loc)
.addStringTemplate("%unit%", getLabel())
.addStringTemplate("%repairLocation%",
loc.getLocationLabelFor(owner)));
setState(UnitState.ACTIVE);
}
cs.addPartial(See.only(owner), this,
"hitPoints", String.valueOf(this.getHitPoints()));
}
/**
* 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} 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) {
final Player player = getOwner();
final Game game = getGame();
final CombatModel combatModel = game.getCombatModel();
final boolean pirate = hasAbility(Ability.PIRACY);
Unit attacker = null;
double 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:
// FIXME: 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 : transform(tile.getUnits(), u ->
(u.isNaval()
&& ((u.isOffensiveUnit() && player.atWarWith(enemy))
|| pirate || u.hasAbility(Ability.PIRACY))))) {
double power = combatModel.getOffencePower(enemyUnit, this);
totalAttackPower += power;
if (power > attackPower) {
attacker = enemyUnit;
attackPower = power;
}
}
}
if (attacker != null) {
double defencePower = combatModel.getDefencePower(attacker, this);
double totalProbability = totalAttackPower + defencePower;
if (randomInt(logger, "Slowed", random,
(int)Math.round(totalProbability + 1)) < totalAttackPower) {
int diff = Math.max(0,
(int)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} to add changes to.
*/
private void csNativeBurialGround(ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer)getOwner();
Tile tile = getTile();
ServerPlayer indianPlayer = (ServerPlayer)tile.getOwner();
serverPlayer.csContact(indianPlayer, cs);
indianPlayer.csModifyTension(serverPlayer,
Tension.Level.HATEFUL.getLimit(), cs);//+til
serverPlayer.csChangeStance(Stance.WAR, indianPlayer, true, cs);
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
RumourType.BURIAL_GROUND.getDescriptionKey(),
serverPlayer, this)
.addStringTemplate("%nation%", indianPlayer.getNationLabel()));
}
/**
* Explore a lost city.
*
* @param random A pseudo-random number source.
* @param cs A {@code ChangeSet} to add changes to.
* @return True if the unit survives.
*/
private boolean csExploreLostCityRumour(Random random, ChangeSet cs) {
ServerPlayer serverPlayer = (ServerPlayer) getOwner();
Tile tile = getTile();
LostCityRumour lostCity = tile.getLostCityRumour();
if (lostCity == null) return true;
Game game = getGame();
Specification spec = game.getSpecification();
int difficulty = spec.getInteger(GameOptions.RUMOUR_DIFFICULTY);
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, 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 (getUnitChange(UnitChangeType.LOST_CITY) != null) {
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 nothing = false;
while (!done) {
rumour = lostCity.chooseType(this, random);
switch (rumour) {
case NOTHING: // Do not accept nothing-result the first time.
if (nothing) {
done = true;
} else {
nothing = true;
}
break;
case EXPEDITION_VANISHES: case TRIBAL_CHIEF:
done = true;
break;
case RUINS:
done = true;
// Misiulo confirms that in Col1 deSoto does *not*
// protect against a burial ground at the same
// time as a ruins find!
if (randomInt(logger, "Ruins+Burial", random, 100)
>= spec.getPercentage(GameOptions.BAD_RUMOUR)) break;
// Fall through
case BURIAL_GROUND:
done = tile.getOwner() != null
&& tile.getOwner().isIndian();
break;
default:
; // unacceptable result for mounds
}
}
}
logger.info("Unit " + getId() + " is exploring rumour " + rumour);
boolean result = true;
String key = rumour.getDescriptionKey();
switch (rumour) {
case BURIAL_GROUND:
csNativeBurialGround(cs);
break;
case EXPEDITION_VANISHES:
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer));
result = false;
break;
case NOTHING:
cs.addMessage(serverPlayer,
lostCity.getNothingMessage(serverPlayer, mounds, random));
break;
case LEARN:
StringTemplate oldName = getLabel();
UnitTypeChange uc = getRandomMember(logger, "Choose learn",
spec.getUnitChanges(UnitChangeType.LOST_CITY, getType()),
random);
changeType(uc.to);//-vis(serverPlayer)
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer, this)
.addStringTemplate("%unit%", oldName)
.addNamed("%type%", getType()));
break;
case TRIBAL_CHIEF:
int chiefAmount = randomInt(logger, "Chief base amount",
random, dx * 10) + dx * 5;
serverPlayer.modifyGold(chiefAmount);
cs.addPartial(See.only(serverPlayer), serverPlayer,
"gold", String.valueOf(serverPlayer.getGold()),
"score", String.valueOf(serverPlayer.getScore()));
if (mounds) key = rumour.getAlternateDescriptionKey("mounds");
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer, this)
.addAmount("%money%", chiefAmount));
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
break;
case COLONIST:
List<UnitType> foundTypes
= spec.getUnitTypesWithAbility(Ability.FOUND_IN_LOST_CITY);
unitType = getRandomMember(logger, "Choose found",
foundTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer,
unitType);//-vis: safe, scout on tile
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer, newUnit));
break;
case CIBOLA:
String cityName = NameCache.getNextCityOfCibola();
if (cityName != null) {
int treasureAmount = randomInt(logger,
"Base treasure amount", random, dx * 600) + dx * 300;
unitType = getRandomMember(logger, "Choose train",
treasureUnitTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer,
unitType);//-vis: safe, scout on tile
newUnit.setTreasureAmount(treasureAmount);
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer, newUnit)
.addName("%city%", cityName)
.addAmount("%money%", treasureAmount));
cs.addGlobalHistory(game,
new HistoryEvent(game.getTurn(),
HistoryEvent.HistoryEventType.CITY_OF_GOLD, serverPlayer)
.addStringTemplate("%nation%", serverPlayer.getNationLabel())
.addName("%city%", cityName)
.addAmount("%treasure%", treasureAmount));
break;
}
// Fall through, found all the cities of gold.
case RUINS:
int ruinsAmount = randomInt(logger, "Base ruins amount", random,
dx * 2) * 300 + 50;
if (ruinsAmount < 500) { // FIXME: remove magic number
serverPlayer.modifyGold(ruinsAmount);
cs.addPartial(See.only(serverPlayer), serverPlayer,
"gold", String.valueOf(serverPlayer.getGold()),
"score", String.valueOf(serverPlayer.getScore()));
} else {
unitType = getRandomMember(logger, "Choose train",
treasureUnitTypes, random);
newUnit = new ServerUnit(game, tile, serverPlayer,
unitType);//-vis: safe, scout on tile
newUnit.setTreasureAmount(ruinsAmount);
}
if (mounds) key = rumour.getAlternateDescriptionKey("mounds");
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, serverPlayer,
((newUnit != null) ? newUnit : this))
.addAmount("%money%", ruinsAmount));
break;
case FOUNTAIN_OF_YOUTH:
ServerEurope europe = (ServerEurope)serverPlayer.getEurope();
if (europe == null) {
// FoY should now be disabled for non-colonial
// players, but leave this in for now as it is harmless.
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
rumour.getAlternateDescriptionKey("noEurope"),
serverPlayer, this));
} else {
if (serverPlayer.isAI()) { // FIXME: let the AI select
europe.generateFountainRecruits(dx, random);
cs.add(See.only(serverPlayer), europe);
} else {
// Remember, and ask player to select
serverPlayer.setRemainingEmigrants(dx);
cs.add(See.only(serverPlayer),
new FountainOfYouthMessage(dx));
}
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.LOST_CITY_RUMOUR,
key, 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.cacheUnseen();//+til
tile.removeLostCityRumour();//-til
return result;
}
/**
* Check for new contacts at a tile.
*
* @param newTile The {@code Tile} to check.
* @param firstLanding True if this is the special "first landing"
* @param cs A {@code ChangeSet} to update.
*/
public void csNewContactCheck(Tile newTile, boolean firstLanding,
ChangeSet cs) {
final ServerPlayer serverPlayer = (ServerPlayer)this.getOwner();
Set<ServerPlayer> pending = new HashSet<>();
for (Tile t : transform(newTile.getSurroundingTiles(1, 1),
nt -> nt != null && nt.isLand())) {
Settlement settlement = t.getSettlement();
Unit unit = null;
ServerPlayer other = (settlement != null)
? (ServerPlayer)settlement.getOwner()
: ((unit = t.getFirstUnit()) != null)
? (ServerPlayer)unit.getOwner()
: null;
if (other == null
|| other == serverPlayer
|| pending.contains(other)) continue; // No contact
if (serverPlayer.csContact(other, cs)) {
// First contact. Note contact pending because
// European first contact now requires a diplomacy
// interaction to complete before leaving UNCONTACTED
// state.
pending.add(other);
if (serverPlayer.isEuropean()) {
if (other.isIndian()) {
Tile offer = (firstLanding && other.owns(newTile))
? newTile
: null;
serverPlayer.csNativeFirstContact(other, offer, cs);
} else {
serverPlayer.csEuropeanFirstContact(this,
settlement, unit, cs);
}
} else {
if (other.isIndian()) {
; // Do nothing
} else {
other.csNativeFirstContact(serverPlayer, null, cs);
}
}
}
// Initialize alarm for native settlements or units and
// notify of contact.
ServerPlayer contactPlayer = serverPlayer;
IndianSettlement is = (settlement instanceof IndianSettlement)
? (IndianSettlement)settlement
: null;
if (is != null
|| (unit != null
&& (is = unit.getHomeIndianSettlement()) != null)
|| (unit != null
&& (contactPlayer = (ServerPlayer)unit.getOwner())
.isEuropean()
&& (is = getHomeIndianSettlement()) != null
&& is.getTile() != null)) {
Tile copied = is.getTile().getTileToCache();
if (contactPlayer.hasExplored(is.getTile())
&& is.setContacted(contactPlayer)) {//-til
is.getTile().cacheUnseen(copied);//+til
cs.add(See.only(contactPlayer), is);
// First European contact with native settlement.
StringTemplate nation = is.getOwner().getNationLabel();
cs.addMessage(contactPlayer,
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.unit.nativeSettlementContact",
this, is)
.addStringTemplate("%nation%", nation)
.addName("%settlement%", is.getName()));
logger.finest("First contact between "
+ contactPlayer.getId()
+ " and " + is + " at " + newTile);
}
}
csActivateSentries(t, cs);
}
}
/**
* Activate sentried units on a tile.
*
* @param tile The {@code Tile} to activate sentries on.
* @param cs A {@code ChangeSet} to update.
*/
private void csActivateSentries(Tile tile, ChangeSet cs) {
for (Unit u : transform(tile.getUnits(),
matchKey(UnitState.SENTRY, Unit::getState))) {
u.setState(UnitState.ACTIVE);
cs.add(See.perhaps(), u);
}
}
/**
* Move a unit.
*
* @param newTile The {@code Tile} to move to.
* @param random A pseudo-random number source.
* @param cs A {@code ChangeSet} to update.
*/
public void csMove(Tile newTile, Random random, ChangeSet cs) {
final ServerPlayer serverPlayer = (ServerPlayer)getOwner();
// Plan to update tiles that could not be seen before but will
// now be within the line-of-sight.
final Location oldLocation = getLocation();
Set<Tile> oldTiles = getVisibleTileSet();
Set<Tile> newTiles
= serverPlayer.collectNewTiles(newTile, getLineOfSight());
// Update unit state.
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.
if (oldLocation instanceof WorkLocation) {
oldLocation.getTile().cacheUnseen();//+til
}
setLocation(newTile);//-vis(serverPlayer),-til if in colony
if (newTile.hasLostCityRumour() && serverPlayer.isEuropean()
&& !csExploreLostCityRumour(random, cs)) {
this.csRemove(See.perhaps().always(serverPlayer),
oldLocation, cs);//-vis(serverPlayer)
}
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
// Update tiles that are now invisible.
removeInPlace(oldTiles, t -> serverPlayer.canSee(t));
if (!oldTiles.isEmpty()) cs.add(See.only(serverPlayer), oldTiles);
// 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.getTile() != null) {
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()) {
Settlement settlement;
Unit unit = null;
int d;
// Claim land for tribe?
if ((newTile.getOwner() == null
|| (newTile.getOwner().isEuropean()
&& newTile.getOwningSettlement() == null))
&& serverPlayer.isIndian()
&& (settlement = getHomeIndianSettlement()) != null
&& ((d = newTile.getDistanceTo(settlement.getTile()))
< (settlement.getRadius()
+ settlement.getType().getExtraClaimableRadius()))
&& randomInt(logger, "Claim tribal land", random, d + 1) == 0) {
newTile.cacheUnseen();//+til
newTile.changeOwnership(serverPlayer, settlement);//-til
}
// Check for first landing
String newLand = null;
boolean firstLanding = !serverPlayer.isNewLandNamed();
if (serverPlayer.isEuropean() && firstLanding) {
newLand = serverPlayer.getNameForNewLand();
// Set the default value now to prevent multiple attempts.
// The user setNewLandName can override.
serverPlayer.setNewLandName(newLand);
cs.add(See.only(serverPlayer),
new NewLandNameMessage(this, newLand));
logger.finest("First landing for " + serverPlayer
+ " at " + newTile + " with " + this);
}
// Check for new contacts.
csNewContactCheck(newTile, firstLanding, cs);
} else { // water
for (Tile t : transform(newTile.getSurroundingTiles(1, 1),
nt -> (nt != null && !nt.isLand()
&& nt.getFirstUnit() != null
&& nt.getFirstUnit().getOwner() != serverPlayer))) {
csActivateSentries(t, cs);
}
}
// Disembark in colony.
if (isCarrier() && !isEmpty() && newTile.getColony() != null
&& getSpecification().getBoolean(GameOptions.DISEMBARK_IN_COLONY)) {
for (Unit u : getUnitList()) {
((ServerUnit)u).csMove(newTile, random, cs);
}
setMovesLeft(0);
}
// Check for slowing units.
Unit slowedBy = getSlowedBy(newTile, random);
if (slowedBy != null) {
StringTemplate enemy = slowedBy.getApparentOwnerName();
cs.addMessage(serverPlayer,
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.unit.slowed", this, slowedBy)
.addStringTemplate("%unit%",
getLabel(UnitLabelType.NATIONAL))
.addStringTemplate("%enemyUnit%",
slowedBy.getLabel(UnitLabelType.PLAIN))
.addStringTemplate("%enemyNation%", enemy));
}
// Check for region discovery
Region region = newTile.getDiscoverableRegion();
if (serverPlayer.isEuropean() && region != null
&& region.getDiscoverer() == null) {
cs.add(See.only(serverPlayer),
new NewRegionNameMessage(region, newTile, this,
serverPlayer.getNameForRegion(region)));
region.setDiscoverer(getId());
}
}
/**
* Remove this unit from the game.
*
* @param see The visibility of the change.
* @param loc The {@code Location} of the change.
* @param cs A {@code ChangeSet} to update.
*/
public void csRemove(See see, Location loc, ChangeSet cs) {
IndianSettlement is = changeHomeIndianSettlement(null);
if (is != null) cs.add(See.only((ServerPlayer)getOwner()), is);
cs.addRemove(see, loc, this);
this.dispose();
}
// Implement TurnTaker
/**
* New turn for this unit.
*
* @param random A {@code Random} number source.
* @param lb A {@code LogBuilder} to log to.
* @param cs A {@code ChangeSet} to update.
*/
@Override
public void csNewTurn(Random random, LogBuilder lb, ChangeSet cs) {
lb.add(this);
ServerPlayer owner = (ServerPlayer) getOwner();
Specification spec = getSpecification();
Location loc = getLocation();
boolean locDirty = false;
boolean unitDirty = false;
// Attrition. Do it first as the unit might die.
if (getType().hasMaximumAttrition() && loc instanceof Tile
&& !((Tile)loc).hasSettlement()) {
int attrition = getAttrition() + 1;
setAttrition(attrition);
if (attrition > getType().getMaximumAttrition()) {
cs.addMessage(owner,
new ModelMessage(ModelMessage.MessageType.UNIT_LOST,
"model.unit.attrition", this)
.addStringTemplate("%unit%", getLabel())
.addStringTemplate("%location%",
loc.getLocationLabelFor(owner)));
cs.add(See.perhaps(), (Tile)loc);
this.csRemove(See.perhaps().always(owner),
loc, cs);//-vis(owner)
owner.invalidateCanSeeTiles();//+vis(owner)
lb.add(", ");
return;
}
} else {
setAttrition(0);
}
// Check for experience-promotion.
GoodsType produce;
UnitType learn;
UnitTypeChange uc;
if (isInColony()
&& (produce = getWorkType()) != null
&& (learn = spec.getExpertForProducing(produce)) != null
&& learn != getType()
&& (uc = getUnitChange(UnitChangeType.EXPERIENCE,learn)) != null) {
int maximumExperience = getType().getMaximumExperience();
int maxValue = (100 * maximumExperience) / uc.probability;
if (maxValue > 0
&& randomInt(logger, "Experience", random, maxValue)
< Math.min(getExperience(), maximumExperience)) {
StringTemplate oldName = getLabel();
changeType(learn);//-vis: safe within colony
cs.addMessage(owner,
new ModelMessage(ModelMessage.MessageType.UNIT_IMPROVED,
"model.unit.experience", getColony(), this)
.addStringTemplate("%oldName%", oldName)
.addStringTemplate("%unit%", getLabel())
.addName("%colony%", getColony().getName()));
lb.add(" experience upgrade to ", getType());
unitDirty = true;
}
}
// Update moves left.
if (isInMission()) {
getTile().updateIndianSettlement(owner);
setMovesLeft(0);
} else if (isDamaged()) {
setMovesLeft(0);
} else {
setMovesLeft(getInitialMovesLeft());
}
if (getWorkLeft() > 0) {
unitDirty = true;
switch (getState()) {
case IMPROVING:
// Has the improvement been completed already? Do nothing.
TileImprovement ti = getWorkImprovement();
if (ti == null // Another unit on the tile completed it first
|| 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);
if (ti.isRoad() && ti.isComplete()) {
ti.updateRoadConnections(true);
for (Tile t : transform(loc.getTile().getSurroundingTiles(1,1),
Tile::hasRoad)) {
cs.add(See.perhaps(), t);
}
locDirty = true;
}
}
break;
default:
setWorkLeft(getWorkLeft() - 1);
break;
}
if (loc instanceof HighSeas && getOwner().isREF()) {
// Swift travel to America for the REF
setWorkLeft(0);
}
}
if (getState() == UnitState.SKIPPED) {
setState(UnitState.ACTIVE);
unitDirty = true;
}
if (getWorkLeft() <= 0) {
if (getLocation() instanceof HighSeas) {
final Europe europe = owner.getEurope();
final Location dst = getDestination();
Location result = resolveDestination();
if (result == europe) {
lb.add(" arrives in Europe");
if (getTradeRoute() == null) {
setDestination(null);
cs.addMessage(owner,
new ModelMessage(ModelMessage.MessageType.UNIT_ARRIVED,
"model.unit.arriveInEurope",
europe, this)
.addNamed("%europe%", europe));
}
setState(UnitState.ACTIVE);
setLocation(europe);//-vis: safe/Europe
cs.add(See.only(owner), owner.getHighSeas());
locDirty = true;
} else {
if (!(result instanceof Tile)) {
logger.warning("Unit has unsupported destination: "
+ dst + " -> " + result);
result = getFullEntryLocation();
}
Tile tile = result.getTile().getSafeTile(owner, random);
lb.add(" arrives in America at ", tile);
if (dst != null) {
lb.add(" sailing for ", dst);
if (dst instanceof Map) setDestination(null);
}
csMove(tile, random, cs);
locDirty = unitDirty = false; // loc update present
}
} else {
switch (getState()) {
case ACTIVE: case FORTIFIED: case SENTRY: case IN_COLONY:
break; // These states are stable
case IMPROVING:
csImproveTile(random, cs);
setWorkImprovement(null);
locDirty = true;
break;
case FORTIFYING:
setState(UnitState.FORTIFIED);
unitDirty = true;
break;
case SKIPPED: default:
lb.add(" work completed, bad state: ", getState());
setState(UnitState.ACTIVE);
unitDirty = true;
break;
}
}
}
if (locDirty) {
cs.add(See.perhaps(), (FreeColGameObject)getLocation());
} else if (unitDirty) {
cs.add(See.perhaps(), this);
} else {
cs.addPartial(See.only(owner), this,
"movesLeft", String.valueOf(this.getMovesLeft()));
}
lb.add(", ");
}
}