package games.strategy.triplea.ui.screen; import games.strategy.engine.data.GameData; import games.strategy.engine.data.GamePlayer; import games.strategy.engine.data.Territory; import games.strategy.engine.data.Unit; import games.strategy.engine.data.UnitType; import games.strategy.triplea.Properties; import games.strategy.triplea.delegate.Matches; import games.strategy.triplea.formatter.MyFormatter; import games.strategy.triplea.image.MapImage; import games.strategy.triplea.settings.ClientSetting; import games.strategy.triplea.ui.UiContext; import games.strategy.triplea.ui.mapdata.MapData; import games.strategy.triplea.ui.screen.drawable.AbstractDrawable; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import lombok.extern.java.Log; import org.triplea.util.Tuple; /** * Draws units for the associated territory. * * <p>If all units cannot be drawn within the territory bounds, they will be drawn in a single * horizontal row, overflowing to the right of the territory. A solid black line, rooted at the * territory's default placement point, will be drawn under all units in this case. */ @Log public class UnitsDrawer extends AbstractDrawable { private final int count; private final String unitType; private final String playerName; private final Point placementPoint; private final int damaged; private final int bombingUnitDamage; private final boolean disabled; private final boolean overflow; private final String territoryName; private final UiContext uiContext; /** Identifies the location where a nation flag is drawn relative to a unit. */ public enum UnitFlagDrawMode { NONE, SMALL_FLAG, LARGE_FLAG; public UnitFlagDrawMode nextDrawMode() { final var values = values(); return values[(ordinal() + 1) % values.length]; } } public UnitsDrawer( final int count, final String unitType, final String playerName, final Point placementPoint, final int damaged, final int bombingUnitDamage, final boolean disabled, final boolean overflow, final String territoryName, final UiContext uiContext) { this.count = count; this.unitType = unitType; this.playerName = playerName; this.placementPoint = placementPoint; this.damaged = damaged; this.bombingUnitDamage = bombingUnitDamage; this.disabled = disabled; this.overflow = overflow; this.territoryName = territoryName; this.uiContext = uiContext; } public Point getPlacementPoint() { return placementPoint; } public String getPlayer() { return playerName; } @Override public void draw( final Rectangle bounds, final GameData data, final Graphics2D graphics, final MapData mapData) { // If there are too many Units at one point a black line is drawn to make clear which units // belong to where if (overflow) { graphics.setColor(Color.BLACK); graphics.fillRect( placementPoint.x - bounds.x - 2, placementPoint.y - bounds.y + uiContext.getUnitImageFactory().getUnitImageHeight(), uiContext.getUnitImageFactory().getUnitImageWidth() + 2, 3); } final UnitType type = data.getUnitTypeList().getUnitType(unitType); if (type == null) { throw new IllegalStateException("Type not found:" + unitType); } final GamePlayer owner = data.getPlayerList().getPlayerId(playerName); final boolean damagedImage = damaged > 0 || bombingUnitDamage > 0; final Optional<Image> img = uiContext.getUnitImageFactory().getImage(type, owner, damagedImage, disabled); if (img.isEmpty() && !uiContext.isShutDown()) { final String imageQualifier; if (disabled) { imageQualifier = "disabled "; } else if (damagedImage) { imageQualifier = "damaged "; } else { imageQualifier = ""; } log.severe( "MISSING UNIT IMAGE (won't be displayed): " + imageQualifier + type.getName() + " owned by " + owner.getName() + " in " + territoryName); } final int maxRange = new Unit(type, owner, data).getMaxMovementAllowed(); final UnitFlagDrawMode drawMode = ClientSetting.unitFlagDrawMode.getValue().orElse(UnitFlagDrawMode.NONE); if (img.isPresent()) { if (drawMode == UnitFlagDrawMode.LARGE_FLAG) { // If unit is not in the "excluded list" it will get drawn if (maxRange != 0) { final Image flag = uiContext.getFlagImageFactory().getFlag(owner); final int xoffset = img.get().getWidth(null) / 2 - flag.getWidth(null) / 2; final int yoffset = img.get().getHeight(null) / 2 - flag.getHeight(null) / 4 - 5; graphics.drawImage( flag, (placementPoint.x - bounds.x) + xoffset, (placementPoint.y - bounds.y) + yoffset, null); } drawUnit(graphics, img.get(), bounds, data); } else if (drawMode == UnitFlagDrawMode.SMALL_FLAG) { drawUnit(graphics, img.get(), bounds, data); // If unit is not in the "excluded list" it will get drawn if (maxRange != 0) { final Image flag = uiContext.getFlagImageFactory().getSmallFlag(owner); final int xoffset = img.get().getWidth(null) - flag.getWidth(null); final int yoffset = img.get().getHeight(null) - flag.getHeight(null); // This Method draws the Flag in the lower right corner of the unit image. Since the // position is the upper // left corner we have to move the picture up by the height and left by the width. graphics.drawImage( flag, (placementPoint.x - bounds.x) + xoffset, (placementPoint.y - bounds.y) + yoffset, null); } } else { drawUnit(graphics, img.get(), bounds, data); } } // more then 1 unit of this category if (count != 1) { final int stackSize = mapData.getDefaultUnitsStackSize(); if (stackSize > 0) { // Display more units as a stack for (int i = 1; i < count && i < stackSize; i++) { if (img.isPresent()) { graphics.drawImage( img.get(), placementPoint.x + 2 * i - bounds.x, placementPoint.y - 2 * i - bounds.y, null); } } if (count > stackSize) { final String s = String.valueOf(count); final int x = placementPoint.x - bounds.x + 2 * stackSize + uiContext.getUnitImageFactory().getUnitImageWidth() * 6 / 10; final int y = placementPoint.y - 2 * stackSize - bounds.y + uiContext.getUnitImageFactory().getUnitImageHeight() / 3; drawOutlinedText( graphics, s, x, y, MapImage.getPropertyUnitCountColor(), MapImage.getPropertyUnitCountOutline()); } } else { // Display a white number at the bottom of the unit final String s = String.valueOf(count); final int x = placementPoint.x - bounds.x + uiContext.getUnitImageFactory().getUnitCounterOffsetWidth(); final int y = placementPoint.y - bounds.y + uiContext.getUnitImageFactory().getUnitCounterOffsetHeight(); drawOutlinedText( graphics, s, x, y, MapImage.getPropertyUnitCountColor(), MapImage.getPropertyUnitCountOutline()); } } displayHitDamage(bounds, graphics); // Display Factory Damage if (Properties.getDamageFromBombingDoneToUnitsInsteadOfTerritories(data) && Matches.unitTypeCanBeDamaged().test(type)) { displayFactoryDamage(bounds, graphics); } } /** This draws the given image onto the given graphics object. */ private void drawUnit( final Graphics2D graphics, final Image image, final Rectangle bounds, final GameData data) { graphics.drawImage(image, placementPoint.x - bounds.x, placementPoint.y - bounds.y, null); // draw unit icons in top right corner final List<Image> unitIcons = uiContext.getUnitIconImageFactory().getImages(playerName, unitType, data); for (final Image unitIcon : unitIcons) { final int xoffset = image.getWidth(null) - unitIcon.getWidth(null); graphics.drawImage( unitIcon, (placementPoint.x - bounds.x) + xoffset, (placementPoint.y - bounds.y), null); } } private void displayHitDamage(final Rectangle bounds, final Graphics2D graphics) { if (territoryName.length() != 0 && damaged > 1) { final String s = String.valueOf(damaged); final int x = placementPoint.x - bounds.x + uiContext.getUnitImageFactory().getUnitImageWidth() * 3 / 4; final int y = placementPoint.y - bounds.y + uiContext.getUnitImageFactory().getUnitImageHeight() / 4; drawOutlinedText( graphics, s, x, y, MapImage.getPropertyUnitHitDamageColor(), MapImage.getPropertyUnitHitDamageOutline()); } } private void displayFactoryDamage(final Rectangle bounds, final Graphics2D graphics) { if (territoryName.length() != 0 && bombingUnitDamage > 0) { final String s = String.valueOf(bombingUnitDamage); final int x = placementPoint.x - bounds.x + uiContext.getUnitImageFactory().getUnitImageWidth() / 4; final int y = placementPoint.y - bounds.y + uiContext.getUnitImageFactory().getUnitImageHeight() / 4; drawOutlinedText( graphics, s, x, y, MapImage.getPropertyUnitFactoryDamageColor(), MapImage.getPropertyUnitFactoryDamageOutline()); } } public static void drawOutlinedText( final Graphics2D graphics, final String s, final int x, final int y, final Color textColor, final Color outlineColor) { final var font = MapImage.getPropertyMapFont(); if (font.getSize() > 0) { graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); graphics.setFont(font); graphics.setColor(outlineColor); graphics.drawString(s, x - 1, y - 1); graphics.drawString(s, x - 1, y + 1); graphics.drawString(s, x + 1, y + 1); graphics.drawString(s, x + 1, y - 1); graphics.setColor(textColor); graphics.drawString(s, x, y); } } Tuple<Territory, List<Unit>> getUnits(final GameData data) { // note - it may be the case where the territory is being changed as a result to a mouse click, // and the map units // haven't updated yet, so the unit count from the territory wont match the units in count final Territory t = data.getMap().getTerritory(territoryName); final UnitType type = data.getUnitTypeList().getUnitType(unitType); final Predicate<Unit> selectedUnits = Matches.unitIsOfType(type) .and(Matches.unitIsOwnedBy(data.getPlayerList().getPlayerId(playerName))) .and( damaged > 0 ? Matches.unitHasTakenSomeDamage() : Matches.unitHasNotTakenAnyDamage()) .and( bombingUnitDamage > 0 ? Matches.unitHasTakenSomeBombingUnitDamage() : Matches.unitHasNotTakenAnyBombingUnitDamage()); return Tuple.of(t, t.getUnitCollection().getMatches(selectedUnits)); } @Override public DrawLevel getLevel() { return DrawLevel.UNITS_LEVEL; } @Override public String toString() { return "UnitsDrawer for " + count + " " + MyFormatter.pluralize(unitType) + " in " + territoryName; } }