/** * 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.client.gui; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.logging.Logger; import org.freecolandroid.repackaged.java.awt.Component; import org.freecolandroid.repackaged.java.awt.Container; import org.freecolandroid.repackaged.java.awt.Font; import org.freecolandroid.repackaged.java.awt.event.ActionEvent; import org.freecolandroid.repackaged.java.awt.event.ActionListener; import org.freecolandroid.repackaged.javax.swing.JMenu; import org.freecolandroid.repackaged.javax.swing.JMenuItem; import org.freecolandroid.repackaged.javax.swing.JPopupMenu; import org.freecolandroid.repackaged.javax.swing.JSeparator; import net.sf.freecol.FreeCol; import net.sf.freecol.client.FreeColClient; import net.sf.freecol.client.gui.action.UnloadAction; import net.sf.freecol.client.gui.i18n.Messages; import net.sf.freecol.client.gui.panel.ChoiceItem; import net.sf.freecol.client.gui.panel.ReportPanel; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.CombatModel.CombatOdds; 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.IndianSettlement; import net.sf.freecol.common.model.LostCityRumour.RumourType; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Settlement; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.StringTemplate; import net.sf.freecol.common.model.Tension; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.TradeRoute; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.networking.DOMMessage; import net.sf.freecol.server.ai.AIColony; import net.sf.freecol.server.ai.AIGoods; import net.sf.freecol.server.ai.AIUnit; import net.sf.freecol.server.ai.TileImprovementPlan; import net.sf.freecol.server.ai.Wish; import net.sf.freecol.server.ai.mission.TransportMission; import net.sf.freecol.server.model.ServerUnit; /** * Allows the user to obtain more info about a certain tile or to * activate a specific unit on the tile, or perform various debug mode * actions. */ public final class TilePopup extends JPopupMenu { @SuppressWarnings("unused") private static final Logger logger = Logger.getLogger(TilePopup.class.getName()); public static final int UNIT_LINES_IN_FIRST_MENU = 9; public static final int UNIT_LINES_IN_OTHER_MENUS = 19; private final FreeColClient freeColClient; private final GUI gui; private final MapViewer mapViewer; private boolean hasAnItem = false; /** * The constructor that will insert the MenuItems. * @param freeColClient The main controller object for the client. * @param tile The <code>Tile</code> to create a popup for. * The popup menu also appears near this <code>Tile</code>. * @param gui The GUI frontend. * @param gui An object with methods used for making the popup. */ public TilePopup(final FreeColClient freeColClient, final GUI gui, final Tile tile) { super(Messages.message(StringTemplate.template("tile") .addAmount("%x%", tile.getX()) .addAmount("%y%", tile.getY()))); this.freeColClient = freeColClient; this.gui = gui; this.mapViewer = gui.getMapViewer(); final Player player = freeColClient.getMyPlayer(); final Unit activeUnit = gui.getActiveUnit(); if (activeUnit != null) { Tile unitTile = activeUnit.getTile(); JMenuItem gotoMenuItem = null; if (activeUnit.isOffensiveUnit() && unitTile.isAdjacent(tile) && activeUnit.getMoveType(tile).isAttack()) { CombatOdds combatOdds = activeUnit.getGame().getCombatModel() .calculateCombatOdds(activeUnit, tile.getDefendingUnit(activeUnit)); String victoryPercent; // If attacking a settlement, the true odds are never known because units // may be hidden within if (tile.getSettlement() != null || combatOdds.win == CombatOdds.UNKNOWN_ODDS) { victoryPercent = "??"; } else { victoryPercent = Integer.toString((int)(combatOdds.win * 100)); } gotoMenuItem = new JMenuItem(Messages.message(StringTemplate. template("attackTileOdds").addName("%chance%", victoryPercent))); } else if (activeUnit.getSimpleMoveType(unitTile, tile).isLegal()) { //final Image gotoImage = (Image) UIManager.get("cursor.go.image"); //JMenuItem gotoMenuItem = new JMenuItem(Messages.message("gotoThisTile"), new ImageIcon(gotoImage)); gotoMenuItem = new JMenuItem(Messages.message("gotoThisTile")); } if (gotoMenuItem != null) { gotoMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { if (!freeColClient.currentPlayerIsMyPlayer()) { return; } Tile currTile = activeUnit.getTile(); // just checking we are not already at the destination if (currTile==tile) { return; } freeColClient.getInGameController().setDestination(activeUnit, tile); freeColClient.getInGameController().moveToDestination(activeUnit); //if unit did not move, we should show the goto path if(activeUnit.getTile() == currTile){ mapViewer.updateGotoPathForActiveUnit(); } } }); add(gotoMenuItem); hasAnItem = true; } // Add move to Europe entry if the unit can do so if (unitTile == tile && activeUnit.isNaval() && activeUnit.canMoveToEurope()) { JMenuItem europeMenuItem = new JMenuItem(Messages.message(StringTemplate.template("gotoEurope"))); europeMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { if (!freeColClient.currentPlayerIsMyPlayer()) { return; } freeColClient.getInGameController() .moveTo(activeUnit, player.getEurope()); } }); add(europeMenuItem); hasAnItem = true; } if (hasAnItem) addSeparator(); } Settlement settlement = tile.getSettlement(); if (settlement != null) { if (settlement.getOwner() == player) { addColony(((Colony) settlement)); } else if (settlement instanceof IndianSettlement) { addIndianSettlement((IndianSettlement) settlement); } if (hasItem()) { addSeparator(); } } addTile(tile); addSeparator(); int lineCount = 0; int maxUnits = UNIT_LINES_IN_FIRST_MENU; Container currentMenu = this; boolean moreUnits = false; List<Unit> units = tile.getUnitList(); Collections.sort(units, ReportPanel.unitTypeComparator); for (final Unit currentUnit : units) { if (lineCount > maxUnits) { JMenu more = new JMenu(Messages.message("more")); more.setFont(more.getFont().deriveFont(Font.ITALIC)); more.setOpaque(false); currentMenu.add(more); currentMenu = more; moreUnits = true; lineCount = 0; maxUnits = UNIT_LINES_IN_OTHER_MENUS; } lineCount += addUnit(currentMenu, currentUnit, !currentUnit.isUnderRepair(), false); } if (tile.getUnitCount() > 1) { if (moreUnits) { addSeparator(); } JMenuItem activateAllItem = new JMenuItem(Messages.message("activateAllUnits")); activateAllItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { Unit lastUnit = null; for (Unit unit : tile.getUnitList()) { freeColClient.getInGameController().clearOrders(unit); lastUnit = unit; } gui.setActiveUnit(lastUnit); } }); add(activateAllItem); } // START DEBUG if (FreeCol.isInDebugMode() && freeColClient.getFreeColServer() != null) { final Game serverGame = freeColClient.getFreeColServer().getGame(); final Player serverPlayer = (Player) serverGame.getFreeColGameObject(player.getId()); boolean notEmpty = false; addSeparator(); JMenu takeOwnership = new JMenu("Take ownership"); takeOwnership.setOpaque(false); JMenu transportLists = new JMenu("Transport lists"); transportLists.setOpaque(false); for (final Unit currentUnit : tile.getUnitList()) { JMenuItem toMenuItem = new JMenuItem(currentUnit.toString()); toMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { // Server: final Unit serverUnit = (Unit) serverGame.getFreeColGameObject(currentUnit.getId()); serverUnit.setOwner(serverPlayer); for (Unit serverChildUnit : currentUnit.getUnitList()) { serverChildUnit.setOwner(serverPlayer); } freeColClient.getConnectController().reconnect(); } }); takeOwnership.add(toMenuItem); notEmpty = true; if (currentUnit.isCarrier()) { final AIUnit au = freeColClient.getFreeColServer() .getAIMain().getAIUnit(currentUnit); if (au.getMission() != null && au.getMission() instanceof TransportMission) { JMenuItem menuItem = new JMenuItem(currentUnit.toString()); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { gui.showInformationMessage(au.getMission().toString()); } }); transportLists.add(menuItem); } } } if (transportLists.getItemCount() > 0) { add(transportLists); } if (tile.getColony() != null) { if (!notEmpty) { takeOwnership.addSeparator(); } JMenuItem toMenuItem = new JMenuItem(tile.getSettlement().toString()); toMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { // Server: final Tile serverTile = (Tile) serverGame.getFreeColGameObject(tile.getId()); serverTile.getSettlement().changeOwner(serverPlayer); freeColClient.getConnectController().reconnect(); } }); takeOwnership.add(toMenuItem); notEmpty = true; JMenuItem displayColonyPlan = new JMenuItem("Display Colony Plan"); displayColonyPlan.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { // Server: final Tile serverTile = (Tile) serverGame.getFreeColGameObject(tile.getId()); final AIColony ac = freeColClient.getFreeColServer() .getAIMain().getAIColony(serverTile.getColony()); StringBuilder info = new StringBuilder(ac.getColonyPlan().toString()); info.append("\n\nTILE IMPROVEMENTS:\n"); for (TileImprovementPlan tip : ac.getTileImprovementPlans()) { info.append(tip.toString()); info.append("\n"); } info.append("\n\nWISHES:\n"); for (Wish w : ac.getWishes()) { info.append(w.toString()); info.append("\n"); } info.append("\n\nEXPORT GOODS:\n"); for (AIGoods aig : ac.getAIGoods()) { info.append(aig.toString()); info.append("\n"); } gui.showInformationMessage(info.toString()); } }); add(displayColonyPlan); } if (tile.getIndianSettlement() != null) { JMenuItem displayGoods = new JMenuItem("Examine Settlement"); final IndianSettlement is = tile.getIndianSettlement(); displayGoods.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { final IndianSettlement sis = (IndianSettlement) serverGame.getFreeColGameObject(is.getId()); gui.showInformationMessage( debugSummarizeSettlement(serverGame, sis)); } }); add(displayGoods); } if (notEmpty) { add(takeOwnership); hasAnItem = true; } if (tile.hasLostCityRumour()) { JMenuItem rumourItem = new JMenuItem("Set Lost City Rumour type"); rumourItem.setOpaque(false); rumourItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { debugSetRumourType(serverGame, tile); } }); add(rumourItem); } JMenuItem addu = new JMenuItem("Add unit"); addu.setOpaque(false); addu.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { debugAddNewUnitToTile(serverGame, tile); } }); add(addu); if (!tile.isEmpty()) { final List<Unit> tileUnits = tile.getUnitList(); JMenuItem adda = new JMenuItem("Reset moves"); adda.setOpaque(false); adda.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { debugResetUnitsMoves(serverGame, tileUnits); } }); add(adda); } for (Unit u : tile.getUnitList()) { if (u.getSpaceLeft() > 0) { final Unit unit = u; JMenuItem addg = new JMenuItem("Add goods"); addg.setOpaque(false); addg.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { debugAddGoodsToUnit(serverGame, unit); } }); add(addg); break; } } JMenuItem dumpItem = new JMenuItem("Dump tile"); dumpItem.setOpaque(false); dumpItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { System.err.println("\nClient side:"); tile.dumpObject(); System.err.println("\n\nServer side:"); serverGame.getFreeColGameObject(tile.getId()) .dumpObject(); System.err.println("\n"); } }); add(dumpItem); } // END DEBUG Component lastComponent = getComponent(getComponentCount() - 1); if (lastComponent instanceof JSeparator) { remove(lastComponent); } } /** * Adds a unit entry to this popup. * @param menu a <code>Container</code> value * @param unit The unit that will be represented on the popup. * @param enabled The initial state for the menu item. * @param indent Should be <code>true</code> if the text should be * indented on the menu. * @return an <code>int</code> value */ private int addUnit(Container menu, final Unit unit, boolean enabled, boolean indent) { StringTemplate occ; TradeRoute tradeRoute = unit.getTradeRoute(); if (unit.getState() == Unit.UnitState.ACTIVE && unit.getMovesLeft() == 0) { if (unit.isUnderRepair()) { occ = StringTemplate.label(": ") .add("model.unit.occupation.underRepair") .add(Integer.toString(unit.getTurnsForRepair())); } else if (tradeRoute != null) { occ = StringTemplate.label(": ") .add("model.unit.occupation.inTradeRoute") .addName(tradeRoute.getName()); } else { occ = StringTemplate.key("model.unit.occupation.activeNoMovesLeft"); } } else if (unit.getState() == Unit.UnitState.IMPROVING && unit.getWorkImprovement() != null) { occ = StringTemplate.label(": ") .add(unit.getWorkImprovement().getType() + ".occupationString") .add(Integer.toString(unit.getWorkTurnsLeft())); } else if (tradeRoute != null) { occ = StringTemplate.label(": ") .add("model.unit.occupation.inTradeRoute") .add(tradeRoute.getName()); } else if (unit.getDestination() != null) { occ = StringTemplate.key("model.unit.occupation.goingSomewhere"); } else { occ = StringTemplate.key("model.unit.occupation." + unit.getState().toString().toLowerCase()); } String text = (indent ? " " : "") + Messages.message(StringTemplate.template("model.unit.nationUnit") .addStringTemplate("%nation%", unit.getOwner().getNationName()) .addStringTemplate("%unit%", Messages.getLabel(unit))) + " (" + Messages.message(occ) + ")"; JMenuItem menuItem = new JMenuItem(text); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { gui.setActiveUnit(unit); } }); int lineCount = 1; if (indent) { menuItem.setFont(menuItem.getFont().deriveFont(Font.ITALIC)); } menuItem.setEnabled(enabled); menu.add(menuItem); for (Unit passenger : unit.getUnitList()) { lineCount += addUnit(menu, passenger, true, true); } boolean hasGoods = false; for (Goods goods: unit.getGoodsList()) { text = (indent ? " " : " ") + Messages.message(goods.getLabel(true)); menuItem = new JMenuItem(text); menuItem.setFont(menuItem.getFont().deriveFont(Font.ITALIC)); menuItem.setEnabled(false); menu.add(menuItem); lineCount++; hasGoods = true; } if (hasGoods) { JMenuItem dumpItem = new JMenuItem(Messages.message("dumpCargo")); dumpItem.setAction(new UnloadAction(freeColClient, gui, unit)); menu.add(dumpItem); lineCount++; } hasAnItem = true; return lineCount; } /** * Adds a colony entry to this popup. * @param colony The colony that will be represented on the popup. */ private void addColony(final Colony colony) { String name = colony.getNameFor(freeColClient.getMyPlayer()); JMenuItem menuItem = new JMenuItem(Messages.message(name)); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { gui.showColonyPanel(colony); } }); add(menuItem); menuItem = new JMenuItem(Messages.message("rename")); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { freeColClient.getInGameController().rename(colony); } }); add(menuItem); hasAnItem = true; } /** * Adds an indian settlement entry to this popup. * * @param settlement The Indian settlement that will be * represented on the popup. */ private void addIndianSettlement(final IndianSettlement settlement) { String name = settlement.getNameFor(freeColClient.getMyPlayer()); JMenuItem menuItem = new JMenuItem(Messages.message(name)); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { gui.showIndianSettlementPanel(settlement); } }); add(menuItem); hasAnItem = true; } /** * Adds a tile entry to this popup. * * @param tile The tile that will be represented on the popup. */ private void addTile(final Tile tile) { JMenuItem menuItem = new JMenuItem(Messages.message(tile.getNameKey())); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent event) { gui.showTilePanel(tile); } }); add(menuItem); /** * Don't set hasAnItem to true, we want the tile panel to open * automatically whenever there is no other item on the list. */ // hasAnItem = true; } /** * Returns true if this popup has at least one menuitem so that we * know that we can show it to the user. Returns false if there * are no menuitems. * * @return True if this popup has at least one menuitem, false otherwise. */ public boolean hasItem() { return hasAnItem || FreeCol.isInDebugMode(); } /** * Debug action to set the lost city rumour type on a tile. * * @param serverGame The server <code>Game</code> containing the tile. * @param tile The <code>Tile</code> to operate on. */ private void debugSetRumourType(final Game serverGame, Tile tile) { List<ChoiceItem<RumourType>> rumours = new ArrayList<ChoiceItem<RumourType>>(); for (RumourType rumour : RumourType.values()) { if (rumour == RumourType.NO_SUCH_RUMOUR) continue; rumours.add(new ChoiceItem<RumourType>(rumour.toString(), rumour)); } RumourType rumourChoice = gui.showChoiceDialog(null, "Select Lost City Rumour", "Cancel", rumours); tile.getTileItemContainer().getLostCityRumour().setType(rumourChoice); final Tile serverTile = (Tile) serverGame .getFreeColGameObject(tile.getId()); serverTile.getTileItemContainer().getLostCityRumour() .setType(rumourChoice); } /** * Debug action to add a new unit to a tile. * * @param serverGame The server <code>Game</code> containing the tile. * @param tile The <code>Tile</code> to add to. */ private void debugAddNewUnitToTile(final Game serverGame, Tile tile) { Specification spec = serverGame.getSpecification(); List<ChoiceItem<UnitType>> uts = new ArrayList<ChoiceItem<UnitType>>(); for (UnitType t : spec.getUnitTypeList()) { uts.add(new ChoiceItem<UnitType>(Messages.message(t.toString() + ".name"), t)); } UnitType unitChoice = gui.showChoiceDialog(null, "Select Unit Type", "Cancel", uts); if (unitChoice == null) return; Player player = freeColClient.getMyPlayer(); Player serverPlayer = (Player) serverGame .getFreeColGameObject(player.getId()); Tile serverTile = (Tile) serverGame .getFreeColGameObject(tile.getId()); Unit carrier = null; if (!serverTile.isLand() && !unitChoice.isNaval()) { for (Unit u : serverTile.getUnitList()) { if (u.isNaval() && u.getSpaceLeft() >= unitChoice.getSpaceTaken()) { carrier = u; break; } } } ServerUnit serverUnit = new ServerUnit(serverGame, (carrier != null) ? carrier : serverTile, serverPlayer, unitChoice); serverUnit.setMovesLeft(serverUnit.getInitialMovesLeft()); Game game = freeColClient.getGame(); Unit unit = new Unit(game, serverUnit.toXMLElement(DOMMessage.createNewDocument())); if (carrier == null) { tile.add(unit); } else { ((Unit)game.getFreeColGameObject(carrier.getId())).add(unit); } gui.setActiveUnit(unit); player.invalidateCanSeeTiles(); gui.refresh(); } /** * Debug action to reset the moves left of the units on a tile. * * @param serverGame The server <code>Game</code> containing the tile. * @param units The <code>Unit</code>s to reactivate. */ private void debugResetUnitsMoves(final Game serverGame, List<Unit> units) { boolean first = true; for (Unit u : units) { Unit su = (Unit)serverGame.getFreeColGameObject(u.getId()); u.setMovesLeft(u.getInitialMovesLeft()); su.setMovesLeft(su.getInitialMovesLeft()); if (first) { gui.setActiveUnit(u); first = false; } } gui.refresh(); } /** * Debug action to add goods to a unit. * * @param serverGame The server <code>Game</code> containing the tile. * @param tile The <code>Unit</code> to add to. */ private void debugAddGoodsToUnit(final Game serverGame, Unit unit) { Specification spec = serverGame.getSpecification(); List<ChoiceItem<GoodsType>> gtl = new ArrayList<ChoiceItem<GoodsType>>(); for (GoodsType t : spec.getGoodsTypeList()) { if (t.isFoodType() && t != spec.getPrimaryFoodType()) continue; gtl.add(new ChoiceItem<GoodsType>(Messages.message(t.toString() + ".name"), t)); } GoodsType goodsType = gui.showChoiceDialog(null, "Select Goods Type", "Cancel", gtl); if (goodsType == null) return; String amount = gui.showInputDialog(null, StringTemplate.name("Select Goods Amount"), "20", "ok", "cancel", true); if (amount == null) return; int a; try { a = Integer.parseInt(amount); } catch (NumberFormatException nfe) { return; } GoodsType sGoodsType = spec.getGoodsType(goodsType.getId()); GoodsContainer ugc = unit.getGoodsContainer(); GoodsContainer sgc = (GoodsContainer) serverGame .getFreeColGameObject(ugc.getId()); ugc.setAmount(goodsType, a); sgc.setAmount(sGoodsType, a); } /** * Debug action to summarize information about a native settlement * that is normally hidden. * * @param serverGame The server <code>Game</code> containing the * settlement. * @param sis The server version of the <code>IndianSettlement</code> * to summarize. * @return A string summarizing the settlement. */ private String debugSummarizeSettlement(final Game serverGame, final IndianSettlement sis) { Specification spec = serverGame.getSpecification(); StringBuilder sb = new StringBuilder(sis.getName() + "\n"); sb.append("\nAlarm\n"); for (Player p : serverGame.getLiveEuropeanPlayers()) { Tension tension = sis.getAlarm(p); sb.append(Messages.message(p.getNationName()) + " " + ((tension == null) ? "(none)" : Integer.toString(tension.getValue())) + " " + Messages.message(sis.getShortAlarmLevelMessageId(p)) + " " + ((sis.hasSpokenToChief(p)) ? "(spoke to chief)" : "") + "\n"); } sb.append("\nGoods Present\n"); for (Goods goods : sis.getCompactGoods()) { sb.append(Messages.message(goods.getLabel(true)) + "\n"); } sb.append("\nGoods Production\n"); for (GoodsType type : spec.getGoodsTypeList()) { int prod = sis.getProductionOf(type); if (prod > 0) { sb.append(Messages.message(type.getNameKey()) + " " + prod + "\n"); } } sb.append("\nPrices (buy 1/100 / sell 1/100)\n"); GoodsType[] wanted = sis.getWantedGoods(); for (GoodsType type : spec.getGoodsTypeList()) { if (!type.isStorable()) continue; int i; for (i = wanted.length - 1; i >= 0; i--) { if (type == wanted[i]) break; } sb.append(Messages.message(type.getNameKey()) + ": " + sis.getPriceToBuy(type, 1) + "/" + sis.getPriceToBuy(type, 100) + " / " + sis.getPriceToSell(type, 1) + "/" + sis.getPriceToSell(type, 100) + ((i < 0) ? "" : " wanted[" + Integer.toString(i) + "]") + "\n"); } sb.append("\nOwned Units\n"); for (Unit u : sis.getOwnedUnits()) { sb.append(u + "\n"); sb.append(" at " + u.getTile() + "\n"); } sb.append("\nTiles\n"); for (Tile t : sis.getOwnedTiles()) { sb.append(t + "\n"); } return sb.toString(); } }