/** * 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"; } }