Package megamek.client.ui.swing

Source Code of megamek.client.ui.swing.BoardView1$EntitySprite

/*
* MegaMek -
* Copyright (C) 2000,2001,2002,2003,2004,2005,2006,2007,2008 Ben Mazur (bmazur@sev.org)
*
*  This program 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.
*
*  This program 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.
*/

package megamek.client.ui.swing;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.image.FilteredImageSource;
import java.awt.image.ImageObserver;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TimerTask;
import java.util.Vector;

import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.ScrollPaneLayout;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;

import megamek.client.TimerSingleton;
import megamek.client.event.BoardViewEvent;
import megamek.client.event.BoardViewListener;
import megamek.client.event.MechDisplayEvent;
import megamek.client.event.MechDisplayListener;
import megamek.client.ui.IBoardView;
import megamek.client.ui.IDisplayable;
import megamek.client.ui.Messages;
import megamek.client.ui.swing.util.KeyAlphaFilter;
import megamek.client.ui.swing.util.PlayerColors;
import megamek.client.ui.swing.util.StraightArrowPolygon;
import megamek.common.Aero;
import megamek.common.Building;
import megamek.common.Compute;
import megamek.common.Coords;
import megamek.common.Entity;
import megamek.common.GunEmplacement;
import megamek.common.IBoard;
import megamek.common.IEntityMovementMode;
import megamek.common.IEntityMovementType;
import megamek.common.IGame;
import megamek.common.IHex;
import megamek.common.Infantry;
import megamek.common.LosEffects;
import megamek.common.Mech;
import megamek.common.Minefield;
import megamek.common.Mounted;
import megamek.common.MovePath;
import megamek.common.MoveStep;
import megamek.common.PlanetaryConditions;
import megamek.common.Player;
import megamek.common.Protomech;
import megamek.common.SpecialHexDisplay;
import megamek.common.Tank;
import megamek.common.TargetRoll;
import megamek.common.Targetable;
import megamek.common.Terrains;
import megamek.common.UnitLocation;
import megamek.common.WeaponType;
import megamek.common.actions.ArtilleryAttackAction;
import megamek.common.actions.AttackAction;
import megamek.common.actions.ChargeAttackAction;
import megamek.common.actions.ClubAttackAction;
import megamek.common.actions.DfaAttackAction;
import megamek.common.actions.EntityAction;
import megamek.common.actions.KickAttackAction;
import megamek.common.actions.PhysicalAttackAction;
import megamek.common.actions.ProtomechPhysicalAttackAction;
import megamek.common.actions.PunchAttackAction;
import megamek.common.actions.PushAttackAction;
import megamek.common.actions.SearchlightAttackAction;
import megamek.common.actions.WeaponAttackAction;
import megamek.common.event.BoardEvent;
import megamek.common.event.BoardListener;
import megamek.common.event.GameBoardChangeEvent;
import megamek.common.event.GameBoardNewEvent;
import megamek.common.event.GameEntityChangeEvent;
import megamek.common.event.GameEntityNewEvent;
import megamek.common.event.GameEntityRemoveEvent;
import megamek.common.event.GameListener;
import megamek.common.event.GameListenerAdapter;
import megamek.common.event.GameNewActionEvent;
import megamek.common.event.GamePhaseChangeEvent;
import megamek.common.preference.IClientPreferences;
import megamek.common.preference.IPreferenceChangeListener;
import megamek.common.preference.PreferenceChangeEvent;
import megamek.common.preference.PreferenceManager;

/**
* Displays the board; lets the user scroll around and select points on it.
*/
public class BoardView1 extends JPanel implements IBoardView, Scrollable, BoardListener,
        MouseListener, KeyListener, MechDisplayListener, IPreferenceChangeListener {

    private static final long serialVersionUID = -5582195884759007416L;

    private static final int TRANSPARENT = 0xFFFF00FF;

    private static final int BOARD_HEX_CLICK = 1;
    private static final int BOARD_HEX_DOUBLECLICK = 2;
    private static final int BOARD_HEX_DRAG = 3;
    private static final int BOARD_HEX_POPUP = 4;

    // the dimensions of megamek's hex images
    private static final int HEX_W = 84;
    private static final int HEX_H = 72;
    private static final int HEX_WC = HEX_W - HEX_W / 4;

    // line width of the c3 network lines
    private static final int C3_LINE_WIDTH = 1;

    private static Font FONT_9 = new Font("SansSerif", Font.PLAIN, 9); //$NON-NLS-1$
    private static Font FONT_10 = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
    private static Font FONT_12 = new Font("SansSerif", Font.PLAIN, 12); //$NON-NLS-1$

    Dimension hex_size = null;

    private Font font_note = FONT_10;
    private Font font_hexnum = FONT_10;
    private Font font_elev = FONT_9;
    private Font font_minefield = FONT_12;

    IGame game;

    private Dimension boardSize;

    // scrolly stuff:
    private JScrollPane scrollpane = null;

    // entity sprites
    private ArrayList<EntitySprite> entitySprites = new ArrayList<EntitySprite>();

    private HashMap<Integer, EntitySprite> entitySpriteIds = new HashMap<Integer, EntitySprite>();

    // sprites for the three selection cursors
    private CursorSprite cursorSprite;
    private CursorSprite highlightSprite;
    private CursorSprite selectedSprite;
    private CursorSprite firstLOSSprite;
    private CursorSprite secondLOSSprite;

    // sprite for current movement
    private ArrayList<StepSprite> pathSprites = new ArrayList<StepSprite>();

    // vector of sprites for all firing lines
    ArrayList<AttackSprite> attackSprites = new ArrayList<AttackSprite>();

    // vector of sprites for all movement paths (using vectored movement)
    private ArrayList<MovementSprite> movementSprites = new ArrayList<MovementSprite>();

    // vector of sprites for C3 network lines
    private ArrayList<C3Sprite> c3Sprites = new ArrayList<C3Sprite>();

    TilesetManager tileManager = null;

    // polygons for a few things
    Polygon hexPoly;
    Polygon[] facingPolys;
    Polygon[] movementPolys;

    // the player who owns this BoardView's client
    private Player localPlayer = null;

    // should we mark deployment hexes for a player?
    private Player m_plDeployer = null;

    // should be able to turn it off(board editor)
    private boolean useLOSTool = true;

    // Initial scale factor for sprites and map
    float scale = 1.00f;

    // Displayables (Chat box, etc.)
    ArrayList<IDisplayable> displayables = new ArrayList<IDisplayable>();

    // Move units step by step
    private ArrayList<MovingUnit> movingUnits = new ArrayList<MovingUnit>();

    private long moveWait = 0;

    // moving entity sprites
    private ArrayList<MovingEntitySprite> movingEntitySprites = new ArrayList<MovingEntitySprite>();
    private HashMap<Integer, MovingEntitySprite> movingEntitySpriteIds = new HashMap<Integer, MovingEntitySprite>();
    private ArrayList<GhostEntitySprite> ghostEntitySprites = new ArrayList<GhostEntitySprite>();

    protected transient ArrayList<BoardViewListener> boardListeners = new ArrayList<BoardViewListener>();

    // wreck sprites
    private ArrayList<WreckSprite> wreckSprites = new ArrayList<WreckSprite>();

    private Coords rulerStart;
    private Coords rulerEnd;
    private Color rulerStartColor;
    private Color rulerEndColor;

    private Coords lastCursor;
    private Coords highlighted;
    private Coords selected;
    private Coords firstLOS;

    // selected entity and weapon for artillery display
    private Entity selectedEntity = null;
    private Mounted selectedWeapon = null;

    // hexes with ECM effect
    private HashMap<Coords, Integer> ecmHexes = null;

    /**
     * Construct a new board view for the specified game
     */
    public BoardView1(IGame game) throws java.io.IOException {
        this.game = game;

        tileManager = new TilesetManager(this);
        ToolTipManager.sharedInstance().registerComponent(this);

        game.addGameListener(gameListener);
        game.getBoard().addBoardListener(this);
        scheduleRedrawTimer();// call only once
        addMouseListener(this);
        addKeyListener(this);
        MouseMotionListener doScrollRectToVisible = new MouseMotionAdapter() {
            @Override
            public void mouseDragged(MouseEvent e) {
                centerOnHex(getCoordsAt(new Point(e.getX(), e.getY())));
            }
        };
        addMouseMotionListener(doScrollRectToVisible);
        setAutoscrolls(true);

        updateBoardSize();

        hex_size = new Dimension((int) (HEX_W * scale), (int) (HEX_H * scale));

        initPolys();

        cursorSprite = new CursorSprite(Color.cyan);
        highlightSprite = new CursorSprite(Color.white);
        selectedSprite = new CursorSprite(Color.blue);
        firstLOSSprite = new CursorSprite(Color.red);
        secondLOSSprite = new CursorSprite(Color.red);

        PreferenceManager.getClientPreferences().addPreferenceChangeListener(this);

        SpecialHexDisplay.Type.ARTILLERY_HIT.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_INCOMING.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_TARGET.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_ADJUSTED.init(getToolkit());
        SpecialHexDisplay.Type.ARTILLERY_AUTOHIT.init(getToolkit());
    }

    protected final RedrawWorker redrawWorker = new RedrawWorker();

    /**
     * this should only be called once!! this will cause a timer to schedule constant screen updates
     * every 20 milliseconds!
     */
    protected void scheduleRedrawTimer() {
        final TimerTask redraw = new TimerTask() {
            @Override
            public void run() {
                try {
                    SwingUtilities.invokeAndWait(redrawWorker);
                } catch (Exception ie) {
                }
            }
        };
        TimerSingleton.getInstance().schedule(redraw, 20, 20);
    }

    protected void scheduleRedraw() {
        try {
            SwingUtilities.invokeLater(redrawWorker);
        } catch (Exception ie) {
        }
    }

    public void preferenceChange(PreferenceChangeEvent e) {
        if (e.getName().equals(IClientPreferences.MAP_TILESET)) {
            updateBoard();
        }
    }

    /**
     * Adds the specified board listener to receive board events from this board.
     *
     * @param listener
     *            the board listener.
     */
    public void addBoardViewListener(BoardViewListener listener) {
        if (!boardListeners.contains(listener)) {
            boardListeners.add(listener);
        }
    }

    /**
     * Removes the specified board listener.
     *
     * @param listener
     *            the board listener.
     */
    public void removeBoardViewListener(BoardViewListener listener) {
        boardListeners.remove(listener);
    }

    /**
     * Notifies attached board listeners of the event.
     *
     * @param event
     *            the board event.
     */
    public void processBoardViewEvent(BoardViewEvent event) {
        if (boardListeners == null) {
            return;
        }
        for (BoardViewListener l : boardListeners) {
            switch (event.getType()) {
            case BoardViewEvent.BOARD_HEX_CLICKED:
            case BoardViewEvent.BOARD_HEX_DOUBLECLICKED:
            case BoardViewEvent.BOARD_HEX_DRAGGED:
            case BoardViewEvent.BOARD_HEX_POPUP:
                l.hexMoused(event);
                break;
            case BoardViewEvent.BOARD_HEX_CURSOR:
                l.hexCursor(event);
                break;
            case BoardViewEvent.BOARD_HEX_HIGHLIGHTED:
                l.boardHexHighlighted(event);
                break;
            case BoardViewEvent.BOARD_HEX_SELECTED:
                l.hexSelected(event);
                break;
            case BoardViewEvent.BOARD_FIRST_LOS_HEX:
                l.firstLOSHex(event);
                break;
            case BoardViewEvent.BOARD_SECOND_LOS_HEX:
                l.secondLOSHex(event, getFirstLOS());
                break;
            case BoardViewEvent.FINISHED_MOVING_UNITS:
                l.finishedMovingUnits(event);
                break;
            case BoardViewEvent.SELECT_UNIT:
                l.unitSelected(event);
                break;
            }
        }
    }

    void addMovingUnit(Entity entity, Vector<UnitLocation> movePath) {
        if (!movePath.isEmpty()) {
            MovingUnit m = new MovingUnit(entity, movePath);
            movingUnits.add(m);

            GhostEntitySprite ghostSprite = new GhostEntitySprite(entity);
            ghostEntitySprites.add(ghostSprite);

            // Center on the starting hex of the moving unit.
            UnitLocation loc = movePath.get(0);
            centerOnHex(loc.getCoords());
        }
    }

    public void addDisplayable(IDisplayable disp) {
        displayables.add(disp);
    }

    public void removeDisplayable(IDisplayable disp) {
        displayables.remove(disp);
    }

    /**
     * Draw the screen!
     */
    @Override
    public synchronized void paintComponent(Graphics g) {

        if (!isTileImagesLoaded()) {
            g.drawString(Messages.getString("BoardView1.loadingImages"), 20, 50); //$NON-NLS-1$
            if (!tileManager.isStarted()) {
                System.out.println("boardview1: loading images for board"); //$NON-NLS-1$
                tileManager.loadNeededImages(game);
            }
            // wait 1 second, then repaint
            repaint(1000);
            return;
        }

        // draw the board
        drawHexes(g);

        // draw wrecks
        if (GUIPreferences.getInstance().getShowWrecks()) {
            drawSprites(g, wreckSprites);
        }

        // Minefield signs all over the place!
        drawMinefields(g);

        // Artillery targets
        drawArtilleryHexes(g);

        // draw highlight border
        drawSprite(g, highlightSprite);

        // draw cursors
        drawSprite(g, cursorSprite);
        drawSprite(g, selectedSprite);
        drawSprite(g, firstLOSSprite);
        drawSprite(g, secondLOSSprite);

        // draw deployment indicators
        if (m_plDeployer != null) {
            drawDeployment(g);
        }

        // draw C3 links
        drawSprites(g, c3Sprites);

        // draw onscreen entities
        drawSprites(g, entitySprites);

        // draw moving onscreen entities
        drawSprites(g, movingEntitySprites);

        // draw ghost onscreen entities
        drawSprites(g, ghostEntitySprites);

        // draw onscreen attacks
        drawSprites(g, attackSprites);

        // draw movement vectors.
        if (game.useVectorMove() && (game.getPhase() == IGame.Phase.PHASE_MOVEMENT)) {
            drawSprites(g, movementSprites);
        }

        // draw movement, if valid
        drawSprites(g, pathSprites);

        // draw the ruler line
        if (rulerStart != null) {
            Point start = getCentreHexLocation(rulerStart);
            if (rulerEnd != null) {
                Point end = getCentreHexLocation(rulerEnd);
                g.setColor(Color.yellow);
                g.drawLine(start.x, start.y, end.x, end.y);

                g.setColor(rulerEndColor);
                g.fillRect(end.x - 1, end.y - 1, 2, 2);
            }

            g.setColor(rulerStartColor);
            g.fillRect(start.x - 1, start.y - 1, 2, 2);
        }

        // draw all the "displayables"
        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            double width = Math.min(boardSize.getWidth(), scrollpane.getViewport().getSize().getWidth());
            double height = Math.min(boardSize.getHeight(), scrollpane.getViewport().getSize().getHeight());
            Dimension drawDimension = new Dimension();
            drawDimension.setSize(width, height);
            disp.draw(g,
                    new Point((int)Math.min(boardSize.getWidth(), -getBounds().getX()),
                            (int)Math.min(boardSize.getHeight(),-getBounds().getY())),
                            drawDimension);
        }
    }

    /**
     * Updates the boardSize variable with the proper values for this board.
     */
    private void updateBoardSize() {
        int width = game.getBoard().getWidth() * (int) (HEX_WC * scale) + (int) (HEX_W / 4 * scale);
        int height = game.getBoard().getHeight() * (int) (HEX_H * scale)
                + (int) (HEX_H / 2 * scale);
        boardSize = new Dimension(width, height);
    }

    /**
     * Looks through a vector of buffered images and draws them if they're onscreen.
     */
    private synchronized void drawSprites(Graphics g, ArrayList<? extends Sprite> spriteArrayList) {
        for (Sprite sprite : spriteArrayList) {
            drawSprite(g, sprite);
        }
    }

    /**
     * Draws a sprite, if it is in the current view
     */
    private final void drawSprite(Graphics g, Sprite sprite) {
        Rectangle view = g.getClipBounds();
        if (view.intersects(sprite.getBounds()) && !sprite.hidden) {
            if (!sprite.isReady()) {
                sprite.prepare();
            }
            sprite.drawOnto(g, sprite.getBounds().x, sprite.getBounds().y, this);
        }
    }

    /**
     * Draw an outline around legal deployment hexes
     */
    private void drawDeployment(Graphics g) {
        Rectangle view = g.getClipBounds();
        // only update visible hexes
        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;
        IBoard board = game.getBoard();
        // loop through the hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                Coords c = new Coords(j + drawX, i + drawY);
                Point p = getHexLocation(c);
                if (board.isLegalDeployment(c, m_plDeployer)) {
                    g.setColor(Color.yellow);
                    int[] xcoords = { p.x + (int) (21 * scale), p.x + (int) (62 * scale),
                            p.x + (int) (83 * scale), p.x + (int) (83 * scale),
                            p.x + (int) (62 * scale), p.x + (int) (21 * scale), p.x, p.x };
                    int[] ycoords = { p.y, p.y, p.y + (int) (35 * scale), p.y + (int) (36 * scale),
                            p.y + (int) (71 * scale), p.y + (int) (71 * scale),
                            p.y + (int) (36 * scale), p.y + (int) (35 * scale) };
                    g.drawPolygon(xcoords, ycoords, 8);
                }
            }
        }
    }

    /**
     * returns the weapon selected in the mech display, or null if none selected or it is not
     * artillery or null if the selected entity is not owned
     */
    private Mounted getSelectedArtilleryWeapon() {
        if ((selectedEntity == null) || (selectedWeapon == null)) {
            return null;
        }

        if (!selectedEntity.getOwner().equals(localPlayer)) {
            return null; // Not my business to see this
        }

        if (selectedEntity.getEquipmentNum(selectedWeapon) == -1) {
            return null; // inconsistent state - weapon not on entity
        }

        if (!((selectedWeapon.getType() instanceof WeaponType) && selectedWeapon.getType().hasFlag(
                WeaponType.F_ARTILLERY))) {
            return null; // not artillery
        }

        // otherwise, a weapon is selected, and it is artillery
        return selectedWeapon;
    }

    /**
     * Display artillery modifier in pretargeted hexes
     */
    private void drawArtilleryHexes(Graphics g) {
        Mounted weapon = getSelectedArtilleryWeapon();
        Rectangle view = g.getClipBounds();

        if ((game.getArtillerySize() == 0) && (weapon == null)) {
            return; // nothing to do
        }

        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;

        IBoard board = game.getBoard();
        Image scaledImage;

        // loop through the hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                Coords c = new Coords(j + drawX, i + drawY);
                Point p = getHexLocation(c);

                if (!board.contains(c)) {
                    continue;
                }

                if (weapon != null) {
                    // process targetted hexes
                    int amod = 0;
                    // Check the predesignated hexes
                    if (selectedEntity.getOwner().getArtyAutoHitHexes().contains(c)) {
                        amod = TargetRoll.AUTOMATIC_SUCCESS;
                    } else {
                        amod = selectedEntity.aTracker.getModifier(weapon, c);
                    }

                    if (amod != 0) {

                        // draw the crosshairs
                        if (amod == TargetRoll.AUTOMATIC_SUCCESS) {
                            // predesignated or already hit
                            scaledImage = tileManager
                                    .getArtilleryTarget(TilesetManager.ARTILLERY_AUTOHIT);
                        } else {
                            scaledImage = tileManager
                                    .getArtilleryTarget(TilesetManager.ARTILLERY_ADJUSTED);
                        }

                        g.drawImage(scaledImage, p.x, p.y, this);
                    }
                }
                // process incoming attacks - requires server to update client's
                // view of game

                for (Enumeration<ArtilleryAttackAction> attacks = game.getArtilleryAttacks(); attacks
                        .hasMoreElements();) {
                    ArtilleryAttackAction a = attacks.nextElement();

                    if (a.getTarget(game).getPosition().equals(c)) {
                        scaledImage = tileManager
                                .getArtilleryTarget(TilesetManager.ARTILLERY_INCOMING);
                        g.drawImage(scaledImage, p.x, p.y, this);
                        break; // do not draw multiple times, tooltop will show
                        // all attacks
                    }
                }
            }
        }
    }

    /*
     * NOTENOTENOTE: (itmo) wouldnt this be simpler with two arrays. One with the strings
     * {"BoardView1.thunderblaablaa","BoardView1.Conventi.."} one with the offsets {51,51,42} etc
     * Preferably indexed by an enum: enum{ Conventional, Thunder; } or something?
     */
    /**
     * Writes "MINEFIELD" in minefield hexes...
     */
    private void drawMinefields(Graphics g) {
        Rectangle view = g.getClipBounds();
        // only update visible hexes
        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;

        IBoard board = game.getBoard();
        // loop through the hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                Coords c = new Coords(j + drawX, i + drawY);
                Point p = getHexLocation(c);

                if (!board.contains(c)) {
                    continue;
                }
                if (!game.containsMinefield(c)) {
                    continue;
                }

                Minefield mf = game.getMinefields(c).get(0);

                Image tmpImage = tileManager.getMinefieldSign();
                g.drawImage(tmpImage, p.x + (int) (13 * scale), p.y + (int) (13 * scale), this);

                g.setColor(Color.black);
                int nbrMfs = game.getNbrMinefields(c);
                if (nbrMfs > 1) {
                    drawCenteredString(Messages.getString("BoardView1.Multiple"), //$NON-NLS-1$
                            p.x, p.y + (int) (51 * scale), font_minefield, g);
                } else if (nbrMfs == 1) {
                    switch (mf.getType()) {
                    case (Minefield.TYPE_CONVENTIONAL):
                        drawCenteredString(Messages.getString("BoardView1.Conventional"), //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale), font_minefield, g);
                        break;
                    case (Minefield.TYPE_INFERNO):
                        drawCenteredString(
                                Messages.getString("BoardView1.Inferno") + mf.getDensity() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (51 * scale), font_minefield, g);
                        break;
                    case (Minefield.TYPE_ACTIVE):
                        drawCenteredString(
                                Messages.getString("BoardView1.Active") + mf.getDensity() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                p.x, p.y + (int) (51 * scale), font_minefield, g);
                        break;
                    case (Minefield.TYPE_COMMAND_DETONATED):
                        drawCenteredString(Messages.getString("BoardView1.Command-"), //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale), font_minefield, g);
                        drawCenteredString(Messages.getString("BoardView1.detonated"), //$NON-NLS-1$
                                p.x, p.y + (int) (60 * scale), font_minefield, g);
                        break;
                    case (Minefield.TYPE_VIBRABOMB):
                        drawCenteredString(Messages.getString("BoardView1.Vibrabomb"), //$NON-NLS-1$
                                p.x, p.y + (int) (51 * scale), font_minefield, g);
                        if (mf.getPlayerId() == localPlayer.getId()) {
                            drawCenteredString("(" + mf.getSetting() + ")", //$NON-NLS-1$ //$NON-NLS-2$
                                    p.x, p.y + (int) (60 * scale), font_minefield, g);
                        }
                        break;
                    }
                }
            }
        }
    }

    private void drawCenteredString(String string, int x, int y, Font font, Graphics graph) {
        FontMetrics currentMetrics = getFontMetrics(font);
        int stringWidth = currentMetrics.stringWidth(string);

        x += ((hex_size.width - stringWidth) / 2);

        graph.setFont(font);
        graph.drawString(string, x, y);
    }

    /**
     * This method creates an image the size of the entire board (all mapsheets), draws the hexes
     * onto it, and returns that image.
     */
    public Image getEntireBoardImage() {
        Image entireBoard = createImage(boardSize.width, boardSize.height);
        Graphics boardGraph = entireBoard.getGraphics();
        drawHexes(boardGraph);
        boardGraph.dispose();
        return entireBoard;
    }

    /**
     * Redraws all hexes in the specified rectangle
     */
    private void drawHexes(Graphics g) {
        Rectangle view = g.getClipBounds();
        // only update visible hexes
        int drawX = view.x / (int) (HEX_WC * scale) - 1;
        int drawY = view.y / (int) (HEX_H * scale) - 1;

        int drawWidth = view.width / (int) (HEX_WC * scale) + 3;
        int drawHeight = view.height / (int) (HEX_H * scale) + 3;

        g.clearRect(view.x, view.y, view.width, view.height);

        // draw some hexes
        for (int i = 0; i < drawHeight; i++) {
            for (int j = 0; j < drawWidth; j++) {
                drawHex(new Coords(j + drawX, i + drawY), g);
            }
        }
    }

    /**
     * Draws a hex onto the board buffer. This assumes that boardRect is current, and does not check
     * if the hex is visible.
     */
    private void drawHex(Coords c, Graphics boardGraph) {
        if (!game.getBoard().contains(c)) {
            return;
        }

        final IHex hex = game.getBoard().getHex(c);
        final Point hexLoc = getHexLocation(c);

        int level = hex.getElevation();
        int depth = hex.depth();
        int height = Math.max(hex.terrainLevel(Terrains.BLDG_ELEV), hex
                .terrainLevel(Terrains.BRIDGE_ELEV));
        height = Math.max(height, hex.terrainLevel(Terrains.INDUSTRIAL));

        // offset drawing point
        int drawX = hexLoc.x; // - boardRect.x;
        int drawY = hexLoc.y; // - boardRect.y;

        // draw picture
        Image baseImage = tileManager.baseFor(hex);

        boardGraph.drawImage(baseImage, drawX, drawY, this);

        if (tileManager.supersFor(hex) != null) {
            for (Image image : tileManager.supersFor(hex)) {
                boardGraph.drawImage(image, drawX, drawY, this);
            }
        }

        if (ecmHexes != null) {
            Integer tint = ecmHexes.get(c);
            if (tint != null) {
                boardGraph.drawImage(tileManager.getEcmShade(tint.intValue()), drawX, drawY, this);
            }
        }

        if (GUIPreferences.getInstance().getBoolean(GUIPreferences.ADVANCED_DARKEN_MAP_AT_NIGHT)
                && (game.getPlanetaryConditions().getLight() > PlanetaryConditions.L_DAY)
                && !game.isPositionIlluminated(c)) {
            boardGraph.drawImage(tileManager.getNightFog(), drawX, drawY, this);
        }
        boardGraph.setColor(GUIPreferences.getInstance().getMapTextColor());

        // draw special stuff for the hex
        final Collection<SpecialHexDisplay> shdList = game.getBoard().getSpecialHexDisplay(c);
        try {
            if (shdList != null) {
                for (SpecialHexDisplay shd : shdList) {
                    if (shd.drawNow(game.getPhase(), game.getRoundCount())) {
                        boardGraph.drawImage(shd.getType().getDefaultImage(), drawX, drawY, this);
                    }
                }
            }
        } catch (IllegalArgumentException e) {
            System.err.println("Illegal argument exception, probably can't load file.");
            e.printStackTrace();
            drawCenteredString("Loading Error", drawX, drawY + (int) (50 * scale), font_note,
                    boardGraph);
            return;
        }
        // draw hex number
        if (scale >= 0.5) {
            drawCenteredString(c.getBoardNum(), drawX, drawY + (int) (12 * scale), font_hexnum,
                    boardGraph);
        }

        // draw terrain level / water depth / building height
        if (scale > 0.5f) {
            int ypos = 70;
            if (level != 0) {
                drawCenteredString(Messages.getString("BoardView1.LEVEL") + level, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev, boardGraph);
                ypos -= 10;
            }
            if (depth != 0) {
                drawCenteredString(Messages.getString("BoardView1.DEPTH") + depth, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev, boardGraph);
                ypos -= 10;
            }
            if (height > 0) {
                boardGraph.setColor(GUIPreferences.getInstance().getColor(
                        "AdvancedBuildingTextColor"));
                drawCenteredString(Messages.getString("BoardView1.HEIGHT") + height, //$NON-NLS-1$
                        drawX, drawY + (int) (ypos * scale), font_elev, boardGraph);
                ypos -= 10;
            }
        }

        // draw elevation borders
        boardGraph.setColor(Color.black);
        if (drawElevationLine(c, 0)) {
            boardGraph.drawLine(drawX + (int) (21 * scale), drawY, drawX + (int) (62 * scale),
                    drawY);
        }
        if (drawElevationLine(c, 1)) {
            boardGraph.drawLine(drawX + (int) (62 * scale), drawY, drawX + (int) (83 * scale),
                    drawY + (int) (35 * scale));
        }
        if (drawElevationLine(c, 2)) {
            boardGraph.drawLine(drawX + (int) (83 * scale), drawY + (int) (36 * scale), drawX
                    + (int) (62 * scale), drawY + (int) (71 * scale));
        }
        if (drawElevationLine(c, 3)) {
            boardGraph.drawLine(drawX + (int) (62 * scale), drawY + (int) (71 * scale), drawX
                    + (int) (21 * scale), drawY + (int) (71 * scale));
        }
        if (drawElevationLine(c, 4)) {
            boardGraph.drawLine(drawX + (int) (21 * scale), drawY + (int) (71 * scale), drawX,
                    drawY + (int) (36 * scale));
        }
        if (drawElevationLine(c, 5)) {
            boardGraph.drawLine(drawX, drawY + (int) (35 * scale), drawX + (int) (21 * scale),
                    drawY);
        }

        // draw mapsheet borders
        if (GUIPreferences.getInstance().getShowMapsheets()) {
            boardGraph.setColor(GUIPreferences.getInstance().getColor(
                    GUIPreferences.ADVANCED_MAPSHEET_COLOR));
            if (c.x % 16 == 0) {
                // left edge of sheet (edge 4 & 5)
                boardGraph.drawLine(drawX + (int) (21 * scale), drawY + (int) (71 * scale), drawX,
                        drawY + (int) (36 * scale));
                boardGraph.drawLine(drawX, drawY + (int) (35 * scale), drawX + (int) (21 * scale),
                        drawY);
            } else if (c.x % 16 == 15) {
                // right edge of sheet (edge 1 & 2)
                boardGraph.drawLine(drawX + (int) (62 * scale), drawY, drawX + (int) (83 * scale),
                        drawY + (int) (35 * scale));
                boardGraph.drawLine(drawX + (int) (83 * scale), drawY + (int) (36 * scale), drawX
                        + (int) (62 * scale), drawY + (int) (71 * scale));
            }
            if (c.y % 17 == 0) {
                // top edge of sheet (edge 0 and possible 1 & 5)
                boardGraph.drawLine(drawX + (int) (21 * scale), drawY, drawX + (int) (62 * scale),
                        drawY);
                if (c.x % 2 == 0) {
                    boardGraph.drawLine(drawX + (int) (62 * scale), drawY, drawX
                            + (int) (83 * scale), drawY + (int) (35 * scale));
                    boardGraph.drawLine(drawX, drawY + (int) (35 * scale), drawX
                            + (int) (21 * scale), drawY);
                }
            } else if (c.y % 17 == 16) {
                // bottom edge of sheet (edge 3 and possible 2 & 4)
                boardGraph.drawLine(drawX + (int) (62 * scale), drawY + (int) (71 * scale), drawX
                        + (int) (21 * scale), drawY + (int) (71 * scale));
                if (c.x % 2 == 1) {
                    boardGraph.drawLine(drawX + (int) (83 * scale), drawY + (int) (36 * scale),
                            drawX + (int) (62 * scale), drawY + (int) (71 * scale));
                    boardGraph.drawLine(drawX + (int) (21 * scale), drawY + (int) (71 * scale),
                            drawX, drawY + (int) (36 * scale));
                }
            }
            boardGraph.setColor(Color.black);
        }
    }

    /**
     * Returns true if an elevation line should be drawn between the starting hex and the hex in the
     * direction specified. Results should be transitive, that is, if a line is drawn in one
     * direction, it should be drawn in the opposite direction as well.
     */
    private final boolean drawElevationLine(Coords src, int direction) {
        final IHex srcHex = game.getBoard().getHex(src);
        final IHex destHex = game.getBoard().getHexInDir(src, direction);
        return (destHex != null) && (srcHex.floor() != destHex.floor());
    }

    /**
     * Returns the absolute position of the upper-left hand corner of the hex graphic
     */
    private Point getHexLocation(int x, int y) {
        return new Point(x * (int) (HEX_WC * scale), y * (int) (HEX_H * scale)
                + ((x & 1) == 1 ? (int) (HEX_H / 2 * scale) : 0));
    }

    Point getHexLocation(Coords c) {
        return getHexLocation(c.x, c.y);
    }

    /**
     * Returns the absolute position of the centre of the hex graphic
     */
    private Point getCentreHexLocation(int x, int y) {
        Point p = getHexLocation(x, y);
        p.x += (HEX_W / 2 * scale);
        p.y += (HEX_H / 2 * scale);
        return p;
    }

    private Point getCentreHexLocation(Coords c) {
        return getCentreHexLocation(c.x, c.y);
    }

    public void drawRuler(Coords s, Coords e, Color sc, Color ec) {
        rulerStart = s;
        rulerEnd = e;
        rulerStartColor = sc;
        rulerEndColor = ec;

        repaint();
    }

    /**
     * Returns the coords at the specified point
     */
    Coords getCoordsAt(Point p) {
        final int x = (p.x ) / (int) (HEX_WC * scale);
        final int y = ((p.y ) - ((x & 1) == 1 ? (int) (HEX_H / 2 * scale) : 0))
                / (int) (HEX_H * scale);
        return new Coords(x, y);
    }

    public void redrawMovingEntity(Entity entity, Coords position, int facing) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = entitySpriteIds.get(entityId);
        ArrayList<EntitySprite> newSprites;
        HashMap<Integer, EntitySprite> newSpriteIds;

        if (sprite != null) {
            newSprites = new ArrayList<EntitySprite>(entitySprites);
            newSpriteIds = new HashMap<Integer, EntitySprite>(entitySpriteIds);

            newSprites.remove(sprite);

            entitySprites = newSprites;
            entitySpriteIds = newSpriteIds;
        }

        MovingEntitySprite mSprite = movingEntitySpriteIds.get(entityId);
        ArrayList<MovingEntitySprite> newMovingSprites = new ArrayList<MovingEntitySprite>(
                movingEntitySprites);
        HashMap<Integer, MovingEntitySprite> newMovingSpriteIds = new HashMap<Integer, MovingEntitySprite>(
                movingEntitySpriteIds);

        if (mSprite != null) {
            newMovingSprites.remove(mSprite);
        }

        if (entity.getPosition() != null) {
            mSprite = new MovingEntitySprite(entity, position, facing);
            newMovingSprites.add(mSprite);
            newMovingSpriteIds.put(entityId, mSprite);
        }

        movingEntitySprites = newMovingSprites;
        movingEntitySpriteIds = newMovingSpriteIds;
    }

    public boolean isMovingUnits() {
        return movingUnits.size() > 0;
    }

    /**
     * Clears the sprite for an entity and prepares it to be re-drawn. Replaces the old sprite with
     * the new! Try to prevent annoying ConcurrentModificationExceptions
     */
    public void redrawEntity(Entity entity) {
        Integer entityId = new Integer(entity.getId());
        EntitySprite sprite = entitySpriteIds.get(entityId);
        ArrayList<EntitySprite> newSprites = new ArrayList<EntitySprite>(entitySprites);
        HashMap<Integer, EntitySprite> newSpriteIds = new HashMap<Integer, EntitySprite>(
                entitySpriteIds);

        if (sprite != null) {
            newSprites.remove(sprite);
        }
        Coords position = entity.getPosition();
        if (position != null) {
            sprite = new EntitySprite(entity);
            newSprites.add(sprite);
            newSpriteIds.put(entityId, sprite);
        }

        entitySprites = newSprites;
        entitySpriteIds = newSpriteIds;

        for (Iterator<C3Sprite> i = c3Sprites.iterator(); i.hasNext();) {
            final C3Sprite c3sprite = i.next();
            if ((c3sprite.entityId == entity.getId()) || (c3sprite.masterId == entity.getId())) {
                i.remove();
            }
        }

        if (entity.hasC3() || entity.hasC3i()) {
            addC3Link(entity);
        }

        scheduleRedraw();
    }

    /**
     * Clears all old entity sprites out of memory and sets up new ones.
     */
    void redrawAllEntities() {
        ArrayList<EntitySprite> newSprites = new ArrayList<EntitySprite>(game.getNoOfEntities());
        HashMap<Integer, EntitySprite> newSpriteIds = new HashMap<Integer, EntitySprite>(game
                .getNoOfEntities());
        ArrayList<WreckSprite> newWrecks = new ArrayList<WreckSprite>();

        Enumeration<Entity> e = game.getWreckedEntities();
        while (e.hasMoreElements()) {
            Entity entity = e.nextElement();
            if (!(entity instanceof Infantry) && (entity.getPosition() != null)) {
                WreckSprite ws = new WreckSprite(entity);
                newWrecks.add(ws);
            }
        }

        clearC3Networks();
        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            final Entity entity = i.nextElement();
            if (entity.getPosition() == null) {
                continue;
            }

            EntitySprite sprite = new EntitySprite(entity);
            newSprites.add(sprite);
            newSpriteIds.put(new Integer(entity.getId()), sprite);

            if (entity.hasC3() || entity.hasC3i()) {
                addC3Link(entity);
            }
        }

        entitySprites = newSprites;
        entitySpriteIds = newSpriteIds;
        wreckSprites = newWrecks;

        scheduleRedraw();
    }

    /**
     * Moves the cursor to the new position, or hides it, if newPos is null
     */
    private void moveCursor(CursorSprite cursor, Coords newPos) {
        final Rectangle oldBounds = new Rectangle(cursor.getBounds());
        if (newPos != null) {
            cursor.setHexLocation(newPos);
        } else {
            cursor.setOffScreen();
        }
        // repaint affected area
        repaint(oldBounds);
        repaint(cursor.getBounds());
    }

    public void centerOnHex(Coords c) {
        if (null == c) {
            return;
        }
        // the scrollbars auto-correct if we try to set a value that's out of
        // bounds
        Point hexPoint = getCentreHexLocation(c);
        JScrollBar vscroll = scrollpane.getVerticalScrollBar();
        vscroll.setValue(hexPoint.y - vscroll.getVisibleAmount() / 2);
        JScrollBar hscroll = scrollpane.getHorizontalScrollBar();
        hscroll.setValue(hexPoint.x - hscroll.getVisibleAmount() / 2);
        repaint();
    }

    /**
     * Clears the old movement data and draws the new. Since it's less expensive to check for and
     * reuse old step sprites than to make a whole new one, we do that.
     */
    public void drawMovementData(Entity entity, MovePath md) {
        ArrayList<StepSprite> temp = pathSprites;
        MoveStep previousStep = null;

        clearMovementData();

        // need to update the movement sprites based on the move path for this entity
        // only way to do this is to clear and refresh (seems wasteful)

        // first get the color for the vector
        Color col = Color.blue;
        if (md.getLastStep() != null) {
            switch (md.getLastStep().getMovementType()) {
            case IEntityMovementType.MOVE_RUN:
            case IEntityMovementType.MOVE_VTOL_RUN:
            case IEntityMovementType.MOVE_OVER_THRUST:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveRunColor");
                break;
            case IEntityMovementType.MOVE_JUMP:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveJumpColor");
                break;
            case IEntityMovementType.MOVE_ILLEGAL:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveIllegalColor");
                break;
            default:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveDefaultColor");
                break;
            }
        }

        refreshMoveVectors(entity, md, col);

        for (Enumeration<MoveStep> i = md.getSteps(); i.hasMoreElements();) {
            final MoveStep step = i.nextElement();
            // check old movement path for reusable step sprites
            boolean found = false;
            for (StepSprite sprite : temp) {
                if (sprite.getStep().canReuseSprite(step) && !(entity instanceof Aero)) {
                    pathSprites.add(sprite);
                    found = true;
                }
            }
            if (!found) {
                if ((null != previousStep)
                        && ((step.getType() == MovePath.STEP_UP)
                                || (step.getType() == MovePath.STEP_DOWN)
                                || (step.getType() == MovePath.STEP_ACC)
                                || (step.getType() == MovePath.STEP_DEC)
                                || (step.getType() == MovePath.STEP_ACCN) || (step.getType() == MovePath.STEP_DECN))) {
                    // Mark the previous elevation change sprite hidden
                    // so that we can draw a new one in it's place without
                    // having overlap.
                    pathSprites.get(pathSprites.size() - 1).hidden = true;
                }

                // for advanced movement, we always need to hide prior
                // because costs will overlap and we only want the current facing
                if ((previousStep != null) && game.useVectorMove()) {
                    pathSprites.get(pathSprites.size() - 1).hidden = true;
                }

                pathSprites.add(new StepSprite(step));
            }
            previousStep = step;
        }
        repaint(100);
    }

    /**
     * Clears current movement data from the screen
     */
    public void clearMovementData() {
        pathSprites = new ArrayList<StepSprite>();
        repaint();
        refreshMoveVectors();
    }

    public void setLocalPlayer(Player p) {
        localPlayer = p;
    }

    public Player getLocalPlayer() {
        return localPlayer;
    }

    /**
     * Specifies that this should mark the deployment hexes for a player. If the player is set to
     * null, no hexes will be marked.
     */
    public void markDeploymentHexesFor(Player p) {
        m_plDeployer = p;
        repaint(100);
    }

    /**
     * Adds a c3 line to the sprite list.
     */
    public void addC3Link(Entity e) {
        if (e.getPosition() == null) {
            return;
        }

        if (e.hasC3i()) {
            for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
                final Entity fe = i.nextElement();
                if (fe.getPosition() == null) {
                    return;
                }
                if (e.onSameC3NetworkAs(fe) && !fe.equals(e)
                        && !Compute.isAffectedByECM(e, e.getPosition(), fe.getPosition())) {
                    c3Sprites.add(new C3Sprite(e, fe));
                }
            }
        } else if (e.getC3Master() != null) {
            Entity eMaster = e.getC3Master();
            if (eMaster.getPosition() == null) {
                return;
            }

            // ECM cuts off the network
            if (!Compute.isAffectedByECM(e, e.getPosition(), eMaster.getPosition())
                    && !Compute.isAffectedByECM(eMaster, eMaster.getPosition(), eMaster
                            .getPosition())) {
                c3Sprites.add(new C3Sprite(e, e.getC3Master()));
            }
        }
    }

    /**
     * Adds an attack to the sprite list.
     */
    public synchronized void addAttack(AttackAction aa) {
        // do not make a sprite unless we're aware of both entities
        // this is not a great solution but better than a crash
        Entity ae = game.getEntity(aa.getEntityId());
        Targetable t = game.getTarget(aa.getTargetType(), aa.getTargetId());
        if ((ae == null) || (t == null) || (t.getTargetType() == Targetable.TYPE_INARC_POD)
                || (t.getPosition() == null) || (ae.getPosition() == null)) {
            return;
        }

        repaint(100);
        for (AttackSprite sprite : attackSprites) {
            // can we just add this attack to an existing one?
            if ((sprite.getEntityId() == aa.getEntityId())
                    && (sprite.getTargetId() == aa.getTargetId())) {
                // use existing attack, but add this weapon
                if (aa instanceof WeaponAttackAction) {
                    WeaponAttackAction waa = (WeaponAttackAction) aa;
                    if (aa.getTargetType() != Targetable.TYPE_HEX_ARTILLERY) {
                        sprite.addWeapon(waa);
                    } else if (waa.getEntity(game).getOwner().getId() == localPlayer.getId()) {
                        sprite.addWeapon(waa);
                    }
                }
                if (aa instanceof KickAttackAction) {
                    sprite.addWeapon((KickAttackAction) aa);
                }
                if (aa instanceof PunchAttackAction) {
                    sprite.addWeapon((PunchAttackAction) aa);
                }
                if (aa instanceof PushAttackAction) {
                    sprite.addWeapon((PushAttackAction) aa);
                }
                if (aa instanceof ClubAttackAction) {
                    sprite.addWeapon((ClubAttackAction) aa);
                }
                if (aa instanceof ChargeAttackAction) {
                    sprite.addWeapon((ChargeAttackAction) aa);
                }
                if (aa instanceof DfaAttackAction) {
                    sprite.addWeapon((DfaAttackAction) aa);
                }
                if (aa instanceof ProtomechPhysicalAttackAction) {
                    sprite.addWeapon((ProtomechPhysicalAttackAction) aa);
                }
                if (aa instanceof SearchlightAttackAction) {
                    sprite.addWeapon((SearchlightAttackAction) aa);
                }
                return;
            }
        }
        // no re-use possible, add a new one
        // don't add a sprite for an artillery attack made by the other player
        if (aa instanceof WeaponAttackAction) {
            WeaponAttackAction waa = (WeaponAttackAction) aa;
            if (aa.getTargetType() != Targetable.TYPE_HEX_ARTILLERY) {
                attackSprites.add(new AttackSprite(aa));
            } else if (waa.getEntity(game).getOwner().getId() == localPlayer.getId()) {
                attackSprites.add(new AttackSprite(aa));
            }
        } else {
            attackSprites.add(new AttackSprite(aa));
        }
    }

    /** Removes all attack sprites from a certain entity */
    public synchronized void removeAttacksFor(Entity e) {
        if (e == null) {
            return;
        }
        int entityId = e.getId();
        for (Iterator<AttackSprite> i = attackSprites.iterator(); i.hasNext();) {
            AttackSprite sprite = i.next();
            if (sprite.getEntityId() == entityId) {
                i.remove();
            }
        }
        repaint(100);
    }

    /**
     * Clears out all attacks and re-adds the ones in the current game.
     */
    public void refreshAttacks() {
        clearAllAttacks();
        for (Enumeration<EntityAction> i = game.getActions(); i.hasMoreElements();) {
            EntityAction ea = i.nextElement();
            if (ea instanceof AttackAction) {
                addAttack((AttackAction) ea);
            }
        }
        for (Enumeration<AttackAction> i = game.getCharges(); i.hasMoreElements();) {
            EntityAction ea = i.nextElement();
            if (ea instanceof PhysicalAttackAction) {
                addAttack((AttackAction) ea);
            }
        }
        repaint(100);
    }

    public void refreshMoveVectors() {
        clearAllMoveVectors();
        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity e = i.nextElement();
            if (e.getPosition() != null) {
                movementSprites.add(new MovementSprite(e, e.getVectors(), Color.gray, false));
            }
        }
    }

    public void refreshMoveVectors(Entity en, MovePath md, Color col) {
        clearAllMoveVectors();
        // same as normal but when I find the active entity I used the MovePath
        // to get vector
        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity e = i.nextElement();
            if (e.getPosition() != null) {
                if (e.getId() == en.getId()) {
                    movementSprites.add(new MovementSprite(e, md.getFinalVectors(), col, true));
                } else {
                    movementSprites.add(new MovementSprite(e, e.getVectors(), col, false));
                }
            }
        }
    }

    public void clearC3Networks() {
        c3Sprites.clear();
    }

    /**
     * Clears out all attacks that were being drawn
     */
    public void clearAllAttacks() {
        attackSprites.clear();
    }

    /**
     * Clears out all movement vectors that were being drawn
     */
    public void clearAllMoveVectors() {
        movementSprites.clear();
    }

    protected void firstLOSHex(Coords c) {
        if (useLOSTool) {
            moveCursor(secondLOSSprite, null);
            moveCursor(firstLOSSprite, c);
        }
    }

    protected void secondLOSHex(Coords c2, Coords c1) {
        if (useLOSTool) {

            Entity ae = chooseEntity(c1);
            Entity te = chooseEntity(c2);

            StringBuffer message = new StringBuffer();
            LosEffects le;
            if ((ae == null) || (te == null)) {
                boolean mechInFirst = GUIPreferences.getInstance().getMechInFirst();
                boolean mechInSecond = GUIPreferences.getInstance().getMechInSecond();
                LosEffects.AttackInfo ai = new LosEffects.AttackInfo();
                ai.attackPos = c1;
                ai.targetPos = c2;
                ai.attackHeight = mechInFirst ? 1 : 0;
                ai.targetHeight = mechInSecond ? 1 : 0;
                ai.attackAbsHeight = game.getBoard().getHex(c1).floor() + ai.attackHeight;
                ai.targetAbsHeight = game.getBoard().getHex(c2).floor() + ai.targetHeight;
                le = LosEffects.calculateLos(game, ai);
                message
                        .append(Messages
                                .getString(
                                        "BoardView1.Attacker", new Object[] { //$NON-NLS-1$
                                                mechInFirst ? Messages.getString("BoardView1.Mech") : Messages.getString("BoardView1.NonMech"), //$NON-NLS-1$ //$NON-NLS-2$
                                                c1.getBoardNum() }));
                message
                        .append(Messages
                                .getString(
                                        "BoardView1.Target", new Object[] { //$NON-NLS-1$
                                                mechInSecond ? Messages
                                                        .getString("BoardView1.Mech") : Messages.getString("BoardView1.NonMech"), //$NON-NLS-1$ //$NON-NLS-2$
                                                c2.getBoardNum() }));
            } else {
                le = LosEffects.calculateLos(game, ae.getId(), te);
                message.append(Messages.getString("BoardView1.Attacker", new Object[] { //$NON-NLS-1$
                        ae.getDisplayName(), c1.getBoardNum() }));
                message.append(Messages.getString("BoardView1.Target", new Object[] { //$NON-NLS-1$
                        te.getDisplayName(), c2.getBoardNum() }));
            }
            if (le.isBlocked()) {
                message.append(Messages.getString("BoardView1.LOSBlocked", new Object[] { //$NON-NLS-1$
                        new Integer(c1.distance(c2)) }));
            } else {
                message.append(Messages.getString("BoardView1.LOSNotBlocked", new Object[] { //$NON-NLS-1$
                        new Integer(c1.distance(c2)) }));
                if (le.getHeavyWoods() > 0) {
                    message.append(Messages.getString("BoardView1.HeavyWoods", new Object[] { //$NON-NLS-1$
                            new Integer(le.getHeavyWoods()) }));
                }
                if (le.getLightWoods() > 0) {
                    message.append(Messages.getString("BoardView1.LightWoods", new Object[] { //$NON-NLS-1$
                            new Integer(le.getLightWoods()) }));
                }
                if (le.getLightSmoke() > 0) {
                    message.append(Messages.getString("BoardView1.LightSmoke", new Object[] { //$NON-NLS-1$
                            new Integer(le.getLightSmoke()) }));
                }
                if (le.getHeavySmoke() > 0) {
                    message.append(Messages.getString("BoardView1.HeavySmoke", new Object[] { //$NON-NLS-1$
                            new Integer(le.getHeavySmoke()) }));
                }
                if (le.isTargetCover()) {
                    message.append(Messages.getString("BoardView1.TargetPartialCover")); //$NON-NLS-1$
                }
                if (le.isAttackerCover()) {
                    message.append(Messages.getString("BoardView1.AttackerPartialCover")); //$NON-NLS-1$
                }
            }
            JOptionPane.showMessageDialog(getRootPane(), message.toString(), Messages
                    .getString("BoardView1.LOSTitle"), JOptionPane.INFORMATION_MESSAGE);
        }
    }

    /**
     * Initializes the various overlay polygons with their vertices.
     */
    public void initPolys() {
        // hex polygon
        hexPoly = new Polygon();
        hexPoly.addPoint(21, 0);
        hexPoly.addPoint(62, 0);
        hexPoly.addPoint(83, 35);
        hexPoly.addPoint(83, 36);
        hexPoly.addPoint(62, 71);
        hexPoly.addPoint(21, 71);
        hexPoly.addPoint(0, 36);
        hexPoly.addPoint(0, 35);

        // facing polygons
        facingPolys = new Polygon[6];
        facingPolys[0] = new Polygon();
        facingPolys[0].addPoint(41, 3);
        facingPolys[0].addPoint(38, 6);
        facingPolys[0].addPoint(45, 6);
        facingPolys[0].addPoint(42, 3);
        facingPolys[1] = new Polygon();
        facingPolys[1].addPoint(69, 17);
        facingPolys[1].addPoint(64, 17);
        facingPolys[1].addPoint(68, 23);
        facingPolys[1].addPoint(70, 19);
        facingPolys[2] = new Polygon();
        facingPolys[2].addPoint(69, 53);
        facingPolys[2].addPoint(68, 49);
        facingPolys[2].addPoint(64, 55);
        facingPolys[2].addPoint(68, 54);
        facingPolys[3] = new Polygon();
        facingPolys[3].addPoint(41, 68);
        facingPolys[3].addPoint(38, 65);
        facingPolys[3].addPoint(45, 65);
        facingPolys[3].addPoint(42, 68);
        facingPolys[4] = new Polygon();
        facingPolys[4].addPoint(15, 53);
        facingPolys[4].addPoint(18, 54);
        facingPolys[4].addPoint(15, 48);
        facingPolys[4].addPoint(14, 52);
        facingPolys[5] = new Polygon();
        facingPolys[5].addPoint(13, 19);
        facingPolys[5].addPoint(15, 23);
        facingPolys[5].addPoint(19, 17);
        facingPolys[5].addPoint(17, 17);

        // movement polygons
        movementPolys = new Polygon[8];
        movementPolys[0] = new Polygon();
        movementPolys[0].addPoint(41, 65);
        movementPolys[0].addPoint(38, 68);
        movementPolys[0].addPoint(45, 68);
        movementPolys[0].addPoint(42, 65);
        movementPolys[1] = new Polygon();
        movementPolys[1].addPoint(17, 48);
        movementPolys[1].addPoint(12, 48);
        movementPolys[1].addPoint(16, 54);
        movementPolys[1].addPoint(17, 49);
        movementPolys[2] = new Polygon();
        movementPolys[2].addPoint(18, 19);
        movementPolys[2].addPoint(17, 15);
        movementPolys[2].addPoint(13, 21);
        movementPolys[2].addPoint(17, 20);
        movementPolys[3] = new Polygon();
        movementPolys[3].addPoint(41, 6);
        movementPolys[3].addPoint(38, 3);
        movementPolys[3].addPoint(45, 3);
        movementPolys[3].addPoint(42, 6);
        movementPolys[4] = new Polygon();
        movementPolys[4].addPoint(67, 15);
        movementPolys[4].addPoint(66, 19);
        movementPolys[4].addPoint(67, 20);
        movementPolys[4].addPoint(71, 20);
        movementPolys[5] = new Polygon();
        movementPolys[5].addPoint(69, 55);
        movementPolys[5].addPoint(66, 50);
        movementPolys[5].addPoint(67, 49);
        movementPolys[5].addPoint(72, 48);

        movementPolys[6] = new Polygon(); // up arrow with tail
        movementPolys[6].addPoint(35, 44);
        movementPolys[6].addPoint(30, 49);
        movementPolys[6].addPoint(33, 49);
        movementPolys[6].addPoint(33, 53);
        movementPolys[6].addPoint(38, 53);
        movementPolys[6].addPoint(38, 49);
        movementPolys[6].addPoint(41, 49);
        movementPolys[6].addPoint(36, 44);
        movementPolys[7] = new Polygon(); // down arrow with tail
        movementPolys[7].addPoint(34, 53);
        movementPolys[7].addPoint(29, 48);
        movementPolys[7].addPoint(32, 48);
        movementPolys[7].addPoint(32, 44);
        movementPolys[7].addPoint(37, 44);
        movementPolys[7].addPoint(37, 48);
        movementPolys[7].addPoint(40, 48);
        movementPolys[7].addPoint(35, 53);
    }

    synchronized boolean doMoveUnits(long idleTime) {
        boolean movingSomething = false;

        if (movingUnits.size() > 0) {

            moveWait += idleTime;

            if (moveWait > GUIPreferences.getInstance().getInt("AdvancedMoveStepDelay")) {

                ArrayList<MovingUnit> spent = new ArrayList<MovingUnit>();

                for (MovingUnit move : movingUnits) {
                    movingSomething = true;
                    Entity ge = game.getEntity(move.entity.getId());
                    if (move.path.size() > 0) {
                        UnitLocation loc = move.path.get(0);
                        if (ge != null) {
                            redrawMovingEntity(move.entity, loc.getCoords(), loc.getFacing());
                        }
                        move.path.remove(0);
                    } else {
                        if (ge != null) {
                            redrawEntity(ge);
                        }
                        spent.add(move);
                    }

                }

                for (MovingUnit move : spent) {
                    movingUnits.remove(move);
                }
                moveWait = 0;

                if (movingUnits.size() == 0) {
                    movingEntitySpriteIds.clear();
                    movingEntitySprites.clear();
                    ghostEntitySprites.clear();
                    processBoardViewEvent(new BoardViewEvent(this,
                            BoardViewEvent.FINISHED_MOVING_UNITS));
                }
            }
        }
        return movingSomething;
    }

    //
    // KeyListener
    //
    public void keyPressed(KeyEvent ke) {
        JScrollBar vbar = scrollpane.getVerticalScrollBar();
        JScrollBar hbar = scrollpane.getHorizontalScrollBar();
        switch (ke.getKeyCode()) {
        case KeyEvent.VK_NUMPAD7:
            hbar.setValue((int) (hbar.getValue() - HEX_W * scale));
            vbar.setValue((int) (vbar.getValue() - HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD8:
        case KeyEvent.VK_UP:
            vbar.setValue((int) (vbar.getValue() - HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD9:
            hbar.setValue((int) (hbar.getValue() + HEX_W * scale));
            vbar.setValue((int) (vbar.getValue() - HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD1:
            hbar.setValue((int) (hbar.getValue() - HEX_W * scale));
            vbar.setValue((int) (vbar.getValue() + HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD2:
        case KeyEvent.VK_DOWN:
            vbar.setValue((int) (vbar.getValue() + HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD3:
            hbar.setValue((int) (hbar.getValue() + HEX_W * scale));
            vbar.setValue((int) (vbar.getValue() + HEX_H * scale));
            break;
        case KeyEvent.VK_NUMPAD4:
        case KeyEvent.VK_LEFT:
            hbar.setValue((int) (hbar.getValue() - HEX_W * scale));
            break;
        case KeyEvent.VK_NUMPAD6:
        case KeyEvent.VK_RIGHT:
            hbar.setValue((int) (hbar.getValue() + HEX_W * scale));
            break;
        case KeyEvent.VK_NUMPAD5:
            // center on the selected entity
            if (selectedEntity != null) {
                centerOnHex(selectedEntity.getPosition());
            }
            break;
        }

        repaint();
        return;
    }

    public void keyReleased(KeyEvent ke) {
    }

    public void keyTyped(KeyEvent ke) {
    }

    //
    // MouseListener
    //
    public void mousePressed(MouseEvent me) {

        Point point = me.getPoint();
        if (null == point) {
            return;
        }

        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(point), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }
        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            double width = Math.min(boardSize.getWidth(), scrollpane.getViewport().getSize().getWidth());
            double height = Math.min(boardSize.getHeight(), scrollpane.getViewport().getSize().getHeight());
            Dimension dispDimension = new Dimension();
            dispDimension.setSize(width, height);
            // we need to adjust the point, because it should be against the displayable dimension
            Point dispPoint = new Point();
            dispPoint.setLocation(point.x + getBounds().x, point.y + getBounds().y);
            if (disp.isHit(dispPoint, dispDimension)) {
                return;
            }
        }

        // Disable scrolling when ctrl or alt is held down, since this
        // means the user wants to use the LOS/ruler tools.
        int mask = InputEvent.CTRL_MASK | InputEvent.ALT_MASK;
        if (!GUIPreferences.getInstance().getRightDragScroll()
                && !GUIPreferences.getInstance().getAlwaysRightClickScroll()
                && (game.getPhase() == IGame.Phase.PHASE_FIRING)) {
            // In the firing phase, also disable scrolling if
            // the right or middle buttons are clicked, since
            // this means the user wants to activate the
            // popup menu or ruler tool.
            mask |= InputEvent.BUTTON2_MASK | InputEvent.BUTTON3_MASK;
        }

        // disable auto--edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getAutoEdgeScroll()) {
            mask |= InputEvent.BUTTON1_MASK;
        }
        // disable edge-scrolling if no option set
        if (!GUIPreferences.getInstance().getClickEdgeScroll()) {
            mask |= InputEvent.BUTTON3_MASK;
        }

        if (GUIPreferences.getInstance().getRightDragScroll()) {
            mask |= InputEvent.BUTTON2_MASK;
        }

        mouseAction(getCoordsAt(point), BOARD_HEX_DRAG, me.getModifiers());
    }

    public void mouseReleased(MouseEvent me) {
        for (int i = 0; i < displayables.size(); i++) {
            IDisplayable disp = displayables.get(i);
            if (disp.isReleased()) {
                return;
            }
        }

        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }

        // Unless the user has auto-scroll on and is using the left
        // mouse button, no click action should be triggered if the map
        // is being scrolled.
        /*
         * if (scrolled && (me.getModifiers() & InputEvent.BUTTON1_MASK) == 0 ||
         * !GUIPreferences.getInstance().getAutoEdgeScroll()) return;
         */
        if (me.getClickCount() == 1) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_CLICK, me.getModifiers());
        } else {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_DOUBLECLICK, me.getModifiers());
        }
    }

    public void mouseEntered(MouseEvent me) {
    }

    public void mouseExited(MouseEvent me) {
    }

    public void mouseClicked(MouseEvent me) {
        if (me.isPopupTrigger()) {
            mouseAction(getCoordsAt(me.getPoint()), BOARD_HEX_POPUP, me.getModifiers());
            return;
        }
    }

    private class MovingUnit {
        public Entity entity;

        public ArrayList<UnitLocation> path;

        MovingUnit(Entity entity, Vector<UnitLocation> path) {
            this.entity = entity;
            this.path = new ArrayList<UnitLocation>(path);
        }
    }

    /**
     * Everything in the main map view is either the board or it's a sprite displayed on top of the
     * board. Most sprites store a transparent image which they draw onto the screen when told to.
     * Sprites keep a bounds rectangle, so it's easy to tell when they'return onscreen.
     */
    abstract class Sprite implements ImageObserver {
        protected Rectangle bounds;

        protected Image image;

        // Set this to true if you don't want the sprite to be drawn.
        protected boolean hidden = false;

        /**
         * Do any necessary preparation. This is called after creation, but before drawing, when a
         * device context is ready to draw with.
         */
        public abstract void prepare();

        /**
         * When we draw our buffered images, it's necessary to implement the ImageObserver
         * interface. This provides the necesasry functionality.
         */
        public boolean imageUpdate(Image image, int infoflags, int x, int y, int width, int height) {
            if (infoflags == ImageObserver.ALLBITS) {
                prepare();
                repaint();
                return false;
            }
            return true;
        }

        /**
         * Returns our bounding rectangle. The coordinates here are stored with the top left corner
         * of the _board_ being 0, 0, so these do not always correspond to screen coordinates.
         */
        public Rectangle getBounds() {
            return bounds;
        }

        /**
         * Are we ready to draw? By default, checks to see that our buffered image has been created.
         */
        public boolean isReady() {
            return image != null;
        }

        /**
         * Draws this sprite onto the specified graphics context.
         */
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            drawOnto(g, x, y, observer, false);
        }

        public void drawOnto(Graphics g, int x, int y, ImageObserver observer,
                boolean makeTranslucent) {
            if (isReady()) {
                if (makeTranslucent) {
                    Graphics2D g2 = (Graphics2D) g;
                    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f));
                    g2.drawImage(image, x, y, observer);
                    g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f));
                } else {
                    g.drawImage(image, x, y, observer);
                }
            } else {
                // grrr... we'll be ready next time!
                prepare();
            }
        }

        /**
         * Returns true if the point is inside this sprite. Uses board coordinates, not screen
         * coordinates. By default, just checks our bounding rectangle, though some sprites override
         * this for a smaller sensitive area.
         */
        public boolean isInside(Point point) {
            return bounds.contains(point);
        }

        /**
         * Since most sprites being drawn correspond to something in the game, this returns a little
         * info for a tooltip.
         */
        public String[] getTooltip() {
            return null;
        }
    }

    /**
     * Sprite for a cursor. Just a hexagon outline in a specified color.
     */
    private class CursorSprite extends Sprite {
        private Color color;

        private Coords hexLoc;

        public CursorSprite(final Color color) {
            this.color = color;
            bounds = new Rectangle(hexPoly.getBounds().width + 1,
                    hexPoly.getBounds().height + 1);
            image = null;

            // start offscreen
            setOffScreen();
        }

        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage = createImage(bounds.width, bounds.height);
            Graphics graph = tempImage.getGraphics();

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);
            // draw attack poly
            graph.setColor(color);
            graph.drawPolygon(hexPoly);

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }

        public void setOffScreen() {
            bounds.setLocation(-100, -100);
            hexLoc = new Coords(-2, -2);
        }

        public void setHexLocation(Coords hexLoc) {
            this.hexLoc = hexLoc;
            bounds.setLocation(getHexLocation(hexLoc));
        }

        @Override
        public Rectangle getBounds() {
            bounds = new Rectangle(hexPoly.getBounds().width + 1,
                    hexPoly.getBounds().height + 1);
            bounds.setLocation(getHexLocation(hexLoc));

            return bounds;
        }
    }

    private class GhostEntitySprite extends Sprite {
        private Entity entity;

        private Rectangle modelRect;

        public GhostEntitySprite(final Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(shortName) + 1,
                    getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            image = null;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create transparent images in
         * AWT.
         */
        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity), 0, 0, this);

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            return bounds;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            drawOnto(g, x, y, observer, true);
        }

    }

    private class MovingEntitySprite extends Sprite {
        private int facing;

        private Entity entity;

        private Rectangle modelRect;

        public MovingEntitySprite(final Entity entity, final Coords position, final int facing) {
            this.entity = entity;
            this.facing = facing;

            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(shortName) + 1,
                    getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(position));

            bounds = tempBounds;
            image = null;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create transparent images in
         * AWT.
         */
        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity, facing), 0, 0, this);

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }
    }

    /**
     * Sprite for an wreck. Consists of an image, drawn from the Tile Manager and an identification
     * label.
     */
    private class WreckSprite extends Sprite {
        private Entity entity;

        private Rectangle modelRect;

        public WreckSprite(final Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();

            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(shortName) + 1,
                    getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            image = null;
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            return bounds;
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create transparent images in
         * AWT.
         */
        @Override
        public void prepare() {
            // figure out size
            String shortName = entity.getShortName();
            Font font = new Font("SansSerif", Font.PLAIN, 10); //$NON-NLS-1$
            Rectangle tempRect = new Rectangle(47, 55,
                    getFontMetrics(font).stringWidth(shortName) + 1, getFontMetrics(font)
                            .getAscent());

            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // Draw wreck image,if we've got one.
            Image wreck = tileManager.wreckMarkerFor(entity);
            if (null != wreck) {
                graph.drawImage(wreck, 0, 0, this);
            }

            // draw box with shortName
            Color text = Color.lightGray;
            Color bkgd = Color.darkGray;
            Color bord = Color.black;

            graph.setFont(font);
            graph.setColor(bord);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width, tempRect.height);
            tempRect.translate(-1, -1);
            graph.setColor(bkgd);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width, tempRect.height);
            graph.setColor(text);
            graph.drawString(shortName, tempRect.x + 1, tempRect.y + tempRect.height - 1);

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }

        /**
         * Overrides to provide for a smaller sensitive area.
         */
        @Override
        public boolean isInside(Point point) {
            return false;
        }

    }

    /**
     * Sprite for an entity. Changes whenever the entity changes. Consists of an image, drawn from
     * the Tile Manager; facing and possibly secondary facing arrows; armor and internal bars; and
     * an identification label.
     */
    private class EntitySprite extends Sprite {
        private Entity entity;

        private Rectangle entityRect;

        private Rectangle modelRect;

        public EntitySprite(final Entity entity) {
            this.entity = entity;

            String shortName = entity.getShortName();

            if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
                shortName = shortName.concat(" (FL: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            if (entity.getMovementMode() == IEntityMovementMode.SUBMARINE) {
                shortName = shortName.concat(" (Depth: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            int face = entity.isCommander() ? Font.ITALIC : Font.PLAIN;
            Font font = new Font("SansSerif", face, 10); //$NON-NLS-1$
            modelRect = new Rectangle(47, 55, getFontMetrics(font).stringWidth(shortName) + 1,
                    getFontMetrics(font).getAscent());
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));

            bounds = tempBounds;
            entityRect = new Rectangle(bounds.x + (int) (20 * scale), bounds.y
                    + (int) (14 * scale), (int) (44 * scale), (int) (44 * scale));
            image = null;
        }

        @Override
        public Rectangle getBounds() {
            Rectangle tempBounds = new Rectangle(hex_size).union(modelRect);
            tempBounds.setLocation(getHexLocation(entity.getPosition()));
            bounds = tempBounds;

            entityRect = new Rectangle(bounds.x + (int) (20 * scale), bounds.y
                    + (int) (14 * scale), (int) (44 * scale), (int) (44 * scale));

            return bounds;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            if (trackThisEntitiesVisibilityInfo(entity)
                    && !entity.isVisibleToEnemy()
                    && GUIPreferences.getInstance().getBoolean(
                            GUIPreferences.ADVANCED_TRANSLUCENT_HIDDEN_UNITS)) {
                // create final image with translucency
                drawOnto(g, x, y, observer, true);
            } else {
                drawOnto(g, x, y, observer, false);
            }
        }

        /**
         * Creates the sprite for this entity. It is an extra pain to create transparent images in
         * AWT.
         */
        @Override
        public void prepare() {
            // figure out size
            String shortName = entity.getShortName();
            if (entity.getMovementMode() == IEntityMovementMode.VTOL) {
                shortName = shortName.concat(" (FL: ").concat(
                        Integer.toString(entity.getElevation())).concat(")");
            }
            if (PreferenceManager.getClientPreferences().getShowUnitId()) {
                shortName += (Messages.getString("BoardView1.ID") + entity.getId()); //$NON-NLS-1$
            }
            int face = entity.isCommander() ? Font.ITALIC : Font.PLAIN;
            Font font = new Font("SansSerif", face, 10); //$NON-NLS-1$
            Rectangle tempRect = new Rectangle(47, 55,
                    getFontMetrics(font).stringWidth(shortName) + 1, getFontMetrics(font)
                            .getAscent());

            // create image for buffer
            Image tempImage;
            Graphics graph;
            try {
                tempImage = createImage(bounds.width, bounds.height);
                graph = tempImage.getGraphics();
            } catch (NullPointerException ex) {
                // argh! but I want it!
                return;
            }

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // draw entity image
            graph.drawImage(tileManager.imageFor(entity), 0, 0, this);

            // draw box with shortName
            Color text, bkgd, bord;
            if (entity.isDone()) {
                text = Color.lightGray;
                bkgd = Color.darkGray;
                bord = Color.black;
            } else if (entity.isImmobile()) {
                text = Color.darkGray;
                bkgd = Color.black;
                bord = Color.lightGray;
            } else {
                text = Color.black;
                bkgd = Color.lightGray;
                bord = Color.darkGray;
            }
            graph.setFont(font);
            graph.setColor(bord);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width, tempRect.height);
            tempRect.translate(-1, -1);
            graph.setColor(bkgd);
            graph.fillRect(tempRect.x, tempRect.y, tempRect.width, tempRect.height);
            graph.setColor(text);
            graph.drawString(shortName, tempRect.x + 1, tempRect.y + tempRect.height - 1);

            // draw facing
            graph.setColor(Color.white);
            if ((entity.getFacing() != -1)
                    && !((entity instanceof Infantry) && (((Infantry) entity).getDugIn() == Infantry.DUG_IN_NONE))
                    && !((entity instanceof Aero) && ((Aero) entity).isSpheroid() && game.getBoard()
                            .inAtmosphere())) {
                graph.drawPolygon(facingPolys[entity.getFacing()]);
            }

            // determine secondary facing for non-mechs & flipped arms
            int secFacing = entity.getFacing();
            if (!((entity instanceof Mech) || (entity instanceof Protomech))) {
                secFacing = entity.getSecondaryFacing();
            } else if (entity.getArmsFlipped()) {
                secFacing = (entity.getFacing() + 3) % 6;
            }
            // draw red secondary facing arrow if necessary
            if ((secFacing != -1) && (secFacing != entity.getFacing())) {
                graph.setColor(Color.red);
                graph.drawPolygon(facingPolys[secFacing]);
            }

            // Determine if the entity has a locked turret,
            // and if it is a gun emplacement
            boolean turretLocked = false;
            int crewStunned = 0;
            boolean ge = false;
            if (entity instanceof Tank) {
                turretLocked = !((Tank) entity).hasNoTurret() && !entity.canChangeSecondaryFacing();
                crewStunned = ((Tank) entity).getStunnedTurns();
            } else if (entity instanceof GunEmplacement) {
                turretLocked = ((GunEmplacement) entity).hasTurret()
                        && !entity.canChangeSecondaryFacing();
                ge = true;
            }

            // draw condition strings

            if(entity instanceof Aero) {
                Aero a = (Aero)entity;

                //draw altitude if Aero in atmosphere
                if(game.getBoard().inAtmosphere()) {
                    graph.setColor(Color.darkGray);
                    graph.drawString(Integer.toString(a.getElevation()), 26, 15);
                    graph.setColor(Color.PINK);
                    graph.drawString(Integer.toString(a.getElevation()), 25, 14);
                }

                if(a.isRolled()) {
                    // draw "rolled"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.ROLLED"), 18, 15); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.ROLLED"), 17, 14); //$NON-NLS-1$
                }

                if(a.isOutControlTotal() & a.isRandomMove()) {
                    // draw "RANDOM"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.RANDOM"), 18, 35); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.RANDOM"), 17, 34); //$NON-NLS-1$
                } else if (a.isOutControlTotal()) {
                    //draw "CONTROL"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.CONTROL"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.CONTROL"), 17, 38); //$NON-NLS-1$
                }

                if(a.isEvading()) {
                    //draw "EVADE" - can't overlap with out of control
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.EVADE"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.EVADE"), 17, 38); //$NON-NLS-1$
                }
            }

            if (entity.crew.isDead()) {
                // draw "CREW DEAD"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("BoardView1.CrewDead"), 18, 39); //$NON-NLS-1$
                graph.setColor(Color.red);
                graph.drawString(Messages.getString("BoardView1.CrewDead"), 17, 38); //$NON-NLS-1$
            } else if (!ge && entity.isImmobile()) {
                if (entity.isProne()) {
                    // draw "IMMOBILE" and "PRONE"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph.drawString(Messages.getString("BoardView1.PRONE"), 26, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph.drawString(Messages.getString("BoardView1.PRONE"), 25, 47); //$NON-NLS-1$
                } else if (crewStunned > 0) {
                    // draw IMMOBILE and STUNNED
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph.drawString(Messages.getString(
                            "BoardView1.STUNNED", new Object[] { crewStunned }), 22, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph.drawString(Messages.getString(
                            "BoardView1.STUNNED", new Object[] { crewStunned }), 21, 47); //$NON-NLS-1$
                } else if (turretLocked) {
                    // draw "IMMOBILE" and "LOCKED"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 18, 35); //$NON-NLS-1$
                    graph.drawString(Messages.getString("BoardView1.LOCKED"), 22, 48); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 17, 34); //$NON-NLS-1$
                    graph.setColor(Color.yellow);
                    graph.drawString(Messages.getString("BoardView1.LOCKED"), 21, 47); //$NON-NLS-1$
                } else {
                    // draw "IMMOBILE"
                    graph.setColor(Color.darkGray);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 18, 39); //$NON-NLS-1$
                    graph.setColor(Color.red);
                    graph.drawString(Messages.getString("BoardView1.IMMOBILE"), 17, 38); //$NON-NLS-1$
                }
            } else if (entity.isProne()) {
                // draw "PRONE"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("BoardView1.PRONE"), 26, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(Messages.getString("BoardView1.PRONE"), 25, 38); //$NON-NLS-1$
            } else if (crewStunned > 0) {
                // draw STUNNED
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString(
                        "BoardView1.STUNNED", new Object[] { crewStunned }), 22, 48); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(Messages.getString(
                        "BoardView1.STUNNED", new Object[] { crewStunned }), 21, 47); //$NON-NLS-1$
            } else if (turretLocked) {
                // draw "LOCKED"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("BoardView1.LOCKED"), 22, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(Messages.getString("BoardView1.LOCKED"), 21, 38); //$NON-NLS-1$
            }

            // If this unit is being swarmed or is swarming another, say so.
            if (Entity.NONE != entity.getSwarmAttackerId()) {
                // draw "SWARMED"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("BoardView1.SWARMED"), 17, 22); //$NON-NLS-1$
                graph.setColor(Color.red);
                graph.drawString(Messages.getString("BoardView1.SWARMED"), 16, 21); //$NON-NLS-1$
            }

            // If this unit is transporting another, say so.
            if ((entity.getLoadedUnits()).size() > 0) {
                // draw "T"
                graph.setColor(Color.darkGray);
                graph.drawString("T", 20, 71); //$NON-NLS-1$
                graph.setColor(Color.black);
                graph.drawString("T", 19, 70); //$NON-NLS-1$
            }

            // If this unit is stuck, say so.
            if ((entity.isStuck())) {
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("BoardView1.STUCK"), 26, 61); //$NON-NLS-1$
                graph.setColor(Color.orange);
                graph.drawString(Messages.getString("BoardView1.STUCK"), 25, 60); //$NON-NLS-1$

            }

            // If this unit is currently unknown to the enemy, say so.
            if (trackThisEntitiesVisibilityInfo(entity)) {
                if (!entity.isSeenByEnemy()) {
                    // draw "U"
                    graph.setColor(Color.darkGray);
                    graph.drawString("U", 30, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("U", 29, 70); //$NON-NLS-1$
                } else if (!entity.isVisibleToEnemy()
                        && !GUIPreferences.getInstance().getBoolean(
                                GUIPreferences.ADVANCED_TRANSLUCENT_HIDDEN_UNITS)) {
                    // If this unit is currently hidden from the enemy, say so.
                    // draw "H"
                    graph.setColor(Color.darkGray);
                    graph.drawString("H", 30, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("H", 29, 70); //$NON-NLS-1$
                }
            }

            // If hull down, show
            if (entity.isHullDown()) {
                // draw "D"
                graph.setColor(Color.darkGray);
                graph.drawString(Messages.getString("UnitOverview.HULLDOWN"), 15, 39); //$NON-NLS-1$
                graph.setColor(Color.yellow);
                graph.drawString(Messages.getString("UnitOverview.HULLDOWN"), 14, 38); //$NON-NLS-1$
            } else if (entity instanceof Infantry) {
                int dig = ((Infantry) entity).getDugIn();
                if (dig == Infantry.DUG_IN_COMPLETE) {
                    // draw "D"
                    graph.setColor(Color.darkGray);
                    graph.drawString("D", 40, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("D", 39, 70); //$NON-NLS-1$
                } else if (dig != Infantry.DUG_IN_NONE) {
                    // draw "W"
                    graph.setColor(Color.darkGray);
                    graph.drawString("W", 40, 71); //$NON-NLS-1$
                    graph.setColor(Color.black);
                    graph.drawString("W", 39, 70); //$NON-NLS-1$
                }
            }

            // Lets draw our armor and internal status bars
            int baseBarLength = 23;
            int barLength = 0;
            double percentRemaining = 0.00;

            percentRemaining = entity.getArmorRemainingPercent();
            barLength = (int) (baseBarLength * percentRemaining);

            graph.setColor(Color.darkGray);
            graph.fillRect(56, 7, 23, 3);
            graph.setColor(Color.lightGray);
            graph.fillRect(55, 6, 23, 3);
            graph.setColor(getStatusBarColor(percentRemaining));
            graph.fillRect(55, 6, barLength, 3);

            if (!ge) {
                // Gun emplacements don't have internal structure
                percentRemaining = entity.getInternalRemainingPercent();
                barLength = (int) (baseBarLength * percentRemaining);

                graph.setColor(Color.darkGray);
                graph.fillRect(56, 11, 23, 3);
                graph.setColor(Color.lightGray);
                graph.fillRect(55, 10, 23, 3);
                graph.setColor(getStatusBarColor(percentRemaining));
                graph.fillRect(55, 10, barLength, 3);
            }

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }

        /**
         * We only want to show double-blind visibility indicators on our own mechs and teammates
         * mechs (assuming team vision option).
         */
        private boolean trackThisEntitiesVisibilityInfo(Entity e) {
            if (getLocalPlayer() == null) {
                return false;
            }

            if (game.getOptions().booleanOption("double_blind") //$NON-NLS-1$
                    && ((e.getOwner().getId() == getLocalPlayer().getId()) || (game.getOptions()
                            .booleanOption("team_vision") //$NON-NLS-1$
                    && (e.getOwner().getTeam() == getLocalPlayer().getTeam())))) {
                return true;
            }
            return false;
        }

        private Color getStatusBarColor(double percentRemaining) {
            if (percentRemaining <= .25) {
                return Color.red;
            } else if (percentRemaining <= .75) {
                return Color.yellow;
            } else {
                return new Color(16, 196, 16);
            }
        }

        /**
         * Overrides to provide for a smaller sensitive area.
         */
        @Override
        public boolean isInside(Point point) {
            return entityRect.contains(point.x, point.y);
        }

        @Override
        public String[] getTooltip() {
            String[] tipStrings = new String[3];
            StringBuffer buffer;

            buffer = new StringBuffer();
            buffer.append(entity.getChassis()).append(" (") //$NON-NLS-1$
                    .append(entity.getOwner().getName()).append("); ") //$NON-NLS-1$
                    .append(entity.getCrew().getGunnery()).append("/") //$NON-NLS-1$
                    .append(entity.getCrew().getPiloting()).append(
                            Messages.getString("BoardView1.pilot")); //$NON-NLS-1$
            int numAdv = entity.getCrew().countAdvantages();
            boolean isMD = entity.getCrew().countMDImplants() > 0;
            if (numAdv > 0) {
                buffer.append(" <") //$NON-NLS-1$
                        .append(numAdv).append(Messages.getString("BoardView1.advs")); //$NON-NLS-1$
            }
            if (isMD) {
                buffer.append(Messages.getString("BoardView1.md")); //$NON-NLS-1$
            }
            tipStrings[0] = buffer.toString();

            GunEmplacement ge = null;
            if (entity instanceof GunEmplacement) {
                ge = (GunEmplacement) entity;
            }

            buffer = new StringBuffer();
            if (ge == null) {
                buffer.append(Messages.getString("BoardView1.move")) //$NON-NLS-1$
                        .append(entity.getMovementAbbr(entity.moved)).append(":") //$NON-NLS-1$
                        .append(entity.delta_distance).append(" (+") //$NON-NLS-1$
                        .append(Compute.getTargetMovementModifier(game, entity.getId()).getValue())
                        .append(");") //$NON-NLS-1$
                        .append(Messages.getString("BoardView1.Heat")) //$NON-NLS-1$
                        .append(entity.heat);
                if (entity.isCharging()) {
                    buffer.append(" ") //$NON-NLS-1$
                            .append(Messages.getString("BoardView1.charge1")); //$NON-NLS-1$
                }
                if (entity.isMakingDfa()) {
                    buffer.append(" ") //$NON-NLS-1$
                            .append(Messages.getString("BoardBiew1.DFA1")); //$NON-NLS-1$
                }
            } else {
                if (ge.hasTurret() && ge.isTurretLocked()) {
                    buffer.append(Messages.getString("BoardView1.TurretLocked"));
                    if (ge.getFirstWeapon() == -1) {
                        buffer.append(",");
                        buffer.append(Messages.getString("BoardView1.WeaponsDestroyed"));
                    }
                } else if (ge.getFirstWeapon() == -1) {
                    buffer.append(Messages.getString("BoardView1.WeaponsDestroyed"));
                } else {
                    buffer.append(Messages.getString("BoardView1.Operational"));
                }
            }
            if (entity.isDone()) {
                buffer.append(" (").append(Messages.getString("BoardView1.done")).append(")");
            }
            tipStrings[1] = buffer.toString();

            buffer = new StringBuffer();
            if (ge == null) {
                buffer.append(Messages.getString("BoardView1.Armor")) //$NON-NLS-1$
                        .append(entity.getTotalArmor()).append(
                                Messages.getString("BoardView1.internal")) //$NON-NLS-1$
                        .append(entity.getTotalInternal());
            } else {
                buffer.append(Messages.getString("BoardView1.cf")) //$NON-NLS-1$
                        .append(ge.getCurrentCF()).append(
                                Messages.getString("BoardView1.turretArmor")) //$NON-NLS-1$
                        .append(ge.getCurrentTurretArmor());
            }
            tipStrings[2] = buffer.toString();

            return tipStrings;
        }
    }

    /**
     * Sprite for a step in a movement path. Only one sprite should exist for any hex in a path.
     * Contains a colored number, and arrows indicating entering, exiting or turning.
     */
    private class StepSprite extends Sprite {
        private MoveStep step;

        public StepSprite(final MoveStep step) {
            this.step = step;

            // step is the size of the hex that this step is in
            bounds = new Rectangle(getHexLocation(step.getPosition()), hex_size);
            image = null;
        }

        @Override
        public void prepare() {
            // create image for buffer
            Image tempImage = createImage(bounds.width, bounds.height);
            Graphics graph = tempImage.getGraphics();

            // fill with key color
            graph.setColor(new Color(TRANSPARENT));
            graph.fillRect(0, 0, bounds.width, bounds.height);

            // setup some variables
            final Point stepPos = getHexLocation(step.getPosition());
            stepPos.translate(-bounds.x, -bounds.y);
            final Polygon facingPoly = facingPolys[step.getFacing()];
            final Polygon movePoly = movementPolys[step.getFacing()];
            Point offsetCostPos;
            Polygon myPoly;
            Color col;
            // set color
            switch (step.getMovementType()) {
            case IEntityMovementType.MOVE_RUN:
            case IEntityMovementType.MOVE_VTOL_RUN:
            case IEntityMovementType.MOVE_OVER_THRUST:
                if (step.isUsingMASC()) {
                    col = GUIPreferences.getInstance().getColor("AdvancedMoveMASCColor");
                } else {
                    col = GUIPreferences.getInstance().getColor("AdvancedMoveRunColor");
                }
                break;
            case IEntityMovementType.MOVE_JUMP:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveJumpColor");
                break;
            case IEntityMovementType.MOVE_ILLEGAL:
                col = GUIPreferences.getInstance().getColor("AdvancedMoveIllegalColor");
                break;
            default:
                if (step.getType() == MovePath.STEP_BACKWARDS) {
                    col = GUIPreferences.getInstance().getColor("AdvancedMoveBackColor");
                } else {
                    col = GUIPreferences.getInstance().getColor("AdvancedMoveDefaultColor");
                }
                break;
            }

            if (game.useVectorMove()) {
                drawActiveVectors(step, stepPos, graph);
            }

            drawConditions(step, stepPos, graph, col);

            // draw arrows and cost for the step
            switch (step.getType()) {
            case MovePath.STEP_FORWARDS:
            case MovePath.STEP_SWIM:
            case MovePath.STEP_BACKWARDS:
            case MovePath.STEP_CHARGE:
            case MovePath.STEP_DFA:
            case MovePath.STEP_LATERAL_LEFT:
            case MovePath.STEP_LATERAL_RIGHT:
            case MovePath.STEP_LATERAL_LEFT_BACKWARDS:
            case MovePath.STEP_LATERAL_RIGHT_BACKWARDS:
            case MovePath.STEP_DEC:
            case MovePath.STEP_DECN:
            case MovePath.STEP_ACC:
            case MovePath.STEP_ACCN:
            case MovePath.STEP_LOOP:
                // draw arrows showing them entering the next
                myPoly = new Polygon(movePoly.xpoints, movePoly.ypoints, movePoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x + 1, stepPos.y + 1);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                // draw movement cost
                drawMovementCost(step, stepPos, graph, col, true);
                drawRemainingVelocity(step, stepPos, graph, true);
                break;
            case MovePath.STEP_GO_PRONE:
            case MovePath.STEP_HULL_DOWN:
            case MovePath.STEP_DOWN:
            case MovePath.STEP_DIG_IN:
            case MovePath.STEP_FORTIFY:
                // draw arrow indicating dropping prone
                // also doubles as the descent indication
                Polygon downPoly = movementPolys[7];
                myPoly = new Polygon(downPoly.xpoints, downPoly.ypoints, downPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x, stepPos.y);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                offsetCostPos = new Point(stepPos.x + 1, stepPos.y + 15);
                drawMovementCost(step, offsetCostPos, graph, col, false);
                break;
            case MovePath.STEP_GET_UP:
            case MovePath.STEP_UP:
            case MovePath.STEP_CAREFUL_STAND:
                // draw arrow indicating standing up
                // also doubles as the climb indication
                Polygon upPoly = movementPolys[6];
                myPoly = new Polygon(upPoly.xpoints, upPoly.ypoints, upPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x, stepPos.y);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                offsetCostPos = new Point(stepPos.x, stepPos.y + 15);
                drawMovementCost(step, offsetCostPos, graph, col, false);
                break;
            case MovePath.STEP_CLIMB_MODE_ON:
                // draw climb mode indicator
                String climb;
                if (step.getParent().getEntity().getMovementMode() == IEntityMovementMode.WIGE) {
                    climb = Messages.getString("BoardView1.WIGEClimb"); //$NON-NLS-1$
                } else {
                    climb = Messages.getString("BoardView1.Climb"); //$NON-NLS-1$
                }
                if (step.isPastDanger()) {
                    climb = "(" + climb + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int climbX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(climb) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(climb, climbX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(climb, climbX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_CLIMB_MODE_OFF:
                // cancel climb mode indicator
                String climboff;
                if (step.getParent().getEntity().getMovementMode() == IEntityMovementMode.WIGE) {
                    climboff = Messages.getString("BoardView1.WIGEClimbOff"); //$NON-NLS-1$
                } else {
                    climboff = Messages.getString("BoardView1.ClimbOff"); //$NON-NLS-1$
                }
                if (step.isPastDanger()) {
                    climboff = "(" + climboff + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int climboffX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(climboff) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(climboff, climboffX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(climboff, climboffX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_TURN_LEFT:
            case MovePath.STEP_TURN_RIGHT:
            case MovePath.STEP_THRUST:
            case MovePath.STEP_YAW:
            case MovePath.STEP_EVADE:
            case MovePath.STEP_ROLL:
                // draw arrows showing the facing
                myPoly = new Polygon(facingPoly.xpoints, facingPoly.ypoints, facingPoly.npoints);
                graph.setColor(Color.darkGray);
                myPoly.translate(stepPos.x + 1, stepPos.y + 1);
                graph.drawPolygon(myPoly);
                graph.setColor(col);
                myPoly.translate(-1, -1);
                graph.drawPolygon(myPoly);
                if(game.useVectorMove()) {
                    drawMovementCost(step, stepPos, graph, col, false);
                }
                break;
            case MovePath.STEP_LOAD:
                // Announce load.
                String load = Messages.getString("BoardView1.Load"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    load = "(" + load + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int loadX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(load) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(load, loadX, stepPos.y + 39);
                graph.setColor(col);
                graph.drawString(load, loadX - 1, stepPos.y + 38);
                break;
            case MovePath.STEP_LAUNCH:
                // announce launch
                String launch = Messages.getString("BoardView1.Launch"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + launch + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int launchX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(launch) / 2);
                int launchY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(launch, launchX, launchY + 1);
                graph.setColor(col);
                graph.drawString(launch, launchX - 1, launchY);
                break;
            case MovePath.STEP_RECOVER:
                // announce launch
                String recover = Messages.getString("BoardView1.Recover"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + recover + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int recoverX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(recover) / 2);
                int recoverY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(recover, recoverX, recoverY + 1);
                graph.setColor(col);
                graph.drawString(recover, recoverX - 1, recoverY);
                break;
            case MovePath.STEP_JOIN:
                //announce launch
                String join = Messages.getString("BoardView1.Join"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    launch = "(" + join + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int joinX = stepPos.x + 42 - (graph.getFontMetrics(graph.getFont()).stringWidth(join) / 2);
                int joinY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(join, joinX, joinY + 1);
                graph.setColor(col);
                graph.drawString(join, joinX - 1, joinY);
                break;
            case MovePath.STEP_UNLOAD:
                // Announce unload.
                String unload = Messages.getString("BoardView1.Unload"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    unload = "(" + unload + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int unloadX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(unload) / 2);
                int unloadY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(unload, unloadX, unloadY + 1);
                graph.setColor(col);
                graph.drawString(unload, unloadX - 1, unloadY);
                break;
            case MovePath.STEP_HOVER:
                // announce launch
                String hover = Messages.getString("BoardView1.Hover"); //$NON-NLS-1$
                if (step.isPastDanger()) {
                    hover = "(" + hover + ")"; //$NON-NLS-1$ //$NON-NLS-2$
                }
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int hoverX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(hover) / 2);
                int hoverY = stepPos.y + 38 + graph.getFontMetrics(graph.getFont()).getHeight();
                graph.setColor(Color.darkGray);
                graph.drawString(hover, hoverX, hoverY + 1);
                graph.setColor(col);
                graph.drawString(hover, hoverX - 1, hoverY);
                drawMovementCost(step, stepPos, graph, col, false);
                break;
            default:
                break;
            }

            // create final image
            image = createImage(new FilteredImageSource(tempImage.getSource(), new KeyAlphaFilter(
                    TRANSPARENT)));
            graph.dispose();
            tempImage.flush();
        }

        /**
         * draw conditions separate from the step, This allows me to keep
         * conditions on the Aero even when that step is erased (as per advanced
         * movement). For now, just evading and rolling. eventually loading and
         * unloading as well
         */
        private void drawConditions(MoveStep step, Point stepPos, Graphics graph, Color col) {

            if (step.isEvading()) {
                String evade = Messages.getString("BoardView1.Evade"); //$NON-NLS-1$
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int evadeX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(evade) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(evade, evadeX, stepPos.y + 64);
                graph.setColor(col);
                graph.drawString(evade, evadeX - 1, stepPos.y + 63);
            }

            if (step.isRolled()) {
                // Announce roll
                String roll = Messages.getString("BoardView1.Roll"); //$NON-NLS-1$
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                int rollX = stepPos.x + 42
                        - (graph.getFontMetrics(graph.getFont()).stringWidth(roll) / 2);
                graph.setColor(Color.darkGray);
                graph.drawString(roll, rollX, stepPos.y + 18);
                graph.setColor(col);
                graph.drawString(roll, rollX - 1, stepPos.y + 17);
            }

        }

        private void drawActiveVectors(MoveStep step, Point stepPos, Graphics graph) {

            /*
             * TODO: it might be better to move this to the MovementSprite so that it is visible
             * before first step and you can't see it for all entities
             */

            int[] activeXpos = { 39, 59, 59, 40, 19, 19 };
            int[] activeYpos = { 20, 28, 52, 59, 52, 28 };

            int[] v = step.getVectors();
            for (int i = 0; i < 6; i++) {

                String active = Integer.toString(v[i]);
                graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
                graph.setColor(Color.darkGray);
                graph.drawString(active, activeXpos[i] + stepPos.x, activeYpos[i] + stepPos.y);
                graph.setColor(Color.red);
                graph.drawString(active, activeXpos[i] + stepPos.x - 1, activeYpos[i] + stepPos.y
                        - 1);

            }

        }

        @Override
        public Rectangle getBounds() {
            bounds = new Rectangle(getHexLocation(step.getPosition()), hex_size);
            return bounds;
        }

        public MoveStep getStep() {
            return step;
        }

        private void drawRemainingVelocity(MoveStep step, Point stepPos, Graphics graph,
                boolean shiftFlag) {
            String velString = null;
            StringBuffer velStringBuf = new StringBuffer();

            if (!game.useVectorMove()
                    && ((step.getMovementType() == IEntityMovementType.MOVE_SAFE_THRUST) || (step
                            .getMovementType() == IEntityMovementType.MOVE_OVER_THRUST))) {
                velStringBuf.append("(").append(step.getVelocityLeft()).append("/").append(
                        step.getVelocity()).append(")");
            }

            Color col = Color.GREEN;
            if (step.getVelocityLeft() > 0) {
                col = Color.RED;
            }

            // Convert the buffer to a String and draw it.
            velString = velStringBuf.toString();
            graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
            int costX = stepPos.x + 42;
            if (shiftFlag) {
                costX -= (graph.getFontMetrics(graph.getFont()).stringWidth(velString) / 2);
            }
            graph.setColor(Color.darkGray);
            graph.drawString(velString, costX, stepPos.y + 28);
            graph.setColor(col);
            graph.drawString(velString, costX - 1, stepPos.y + 27);

        }

        private void drawMovementCost(MoveStep step, Point stepPos, Graphics graph, Color col,
                boolean shiftFlag) {
            String costString = null;
            StringBuffer costStringBuf = new StringBuffer();
            costStringBuf.append(step.getMpUsed());

            // If the step is using a road bonus, mark it.
            if (step.isOnlyPavement() && (step.getParent().getEntity() instanceof Tank)) {
                costStringBuf.append("+"); //$NON-NLS-1$
            }

            // If the step is dangerous, mark it.
            if (step.isDanger()) {
                costStringBuf.append("*"); //$NON-NLS-1$
            }

            // If the step is past danger, mark that.
            if (step.isPastDanger()) {
                costStringBuf.insert(0, "("); //$NON-NLS-1$
                costStringBuf.append(")"); //$NON-NLS-1$
            }

            if (step.isUsingMASC()) {
                costStringBuf.append("["); //$NON-NLS-1$
                costStringBuf.append(step.getTargetNumberMASC());
                costStringBuf.append("+]"); //$NON-NLS-1$
            }

            if ((step.getMovementType() == IEntityMovementType.MOVE_VTOL_WALK)
                    || (step.getMovementType() == IEntityMovementType.MOVE_VTOL_RUN)
                    || (step.getMovementType() == IEntityMovementType.MOVE_SUBMARINE_WALK)
                    || (step.getMovementType() == IEntityMovementType.MOVE_SUBMARINE_RUN)
                    || (step.getElevation() != 0)) {
                costStringBuf.append("{").append(step.getElevation()).append("}");
            }

            // Convert the buffer to a String and draw it.
            costString = costStringBuf.toString();
            graph.setFont(new Font("SansSerif", Font.PLAIN, 12)); //$NON-NLS-1$
            int costX = stepPos.x + 42;
            if (shiftFlag) {
                costX -= (graph.getFontMetrics(graph.getFont()).stringWidth(costString) / 2);
            }
            graph.setColor(Color.darkGray);
            graph.drawString(costString, costX, stepPos.y + 39);
            graph.setColor(col);
            graph.drawString(costString, costX - 1, stepPos.y + 38);
        }

    }

    /**
     * Sprite and info for a C3 network. Does not actually use the image buffer as this can be
     * horribly inefficient for long diagonal lines.
     */
    private class C3Sprite extends Sprite {
        private Polygon c3Poly;

        protected int entityId;

        protected int masterId;

        protected Entity entityE;

        protected Entity entityM;

        Color spriteColor;

        public C3Sprite(final Entity e, final Entity m) {
            entityE = e;
            entityM = m;
            entityId = e.getId();
            masterId = m.getId();
            spriteColor = PlayerColors.getColor(e.getOwner().getColorIndex());

            if ((e.getPosition() == null) || (m.getPosition() == null)) {
                c3Poly = new Polygon();
                c3Poly.addPoint(0, 0);
                c3Poly.addPoint(1, 0);
                c3Poly.addPoint(0, 1);
                bounds = new Rectangle(c3Poly.getBounds());
                bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
                image = null;
                return;
            }

            makePoly();

            // set bounds
            bounds = new Rectangle(c3Poly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);

            // move poly to upper right of image
            c3Poly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            // set names & stuff

            // nullify image
            image = null;
        }

        @Override
        public void prepare() {
        }

        private void makePoly() {
            // make a polygon
            final Point a = getHexLocation(entityE.getPosition());
            final Point t = getHexLocation(entityM.getPosition());

            final double an = (entityE.getPosition().radian(entityM.getPosition()) + (Math.PI * 1.5))
                    % (Math.PI * 2); // angle
            final double lw = scale * C3_LINE_WIDTH; // line width

            c3Poly = new Polygon();
            c3Poly.addPoint(
                    a.x + (int) (scale * (HEX_W / 2) - (int) Math.round(Math.sin(an) * lw)), a.y
                            + (int) (scale * (HEX_H / 2) + (int) Math.round(Math.cos(an) * lw)));
            c3Poly.addPoint(
                    a.x + (int) (scale * (HEX_W / 2) + (int) Math.round(Math.sin(an) * lw)), a.y
                            + (int) (scale * (HEX_H / 2) - (int) Math.round(Math.cos(an) * lw)));
            c3Poly.addPoint(
                    t.x + (int) (scale * (HEX_W / 2) + (int) Math.round(Math.sin(an) * lw)), t.y
                            + (int) (scale * (HEX_H / 2) - (int) Math.round(Math.cos(an) * lw)));
            c3Poly.addPoint(
                    t.x + (int) (scale * (HEX_W / 2) - (int) Math.round(Math.sin(an) * lw)), t.y
                            + (int) (scale * (HEX_H / 2) + (int) Math.round(Math.cos(an) * lw)));
        }

        @Override
        public Rectangle getBounds() {
            makePoly();
            // set bounds
            bounds = new Rectangle(c3Poly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);

            // move poly to upper right of image
            c3Poly.translate(-bounds.getLocation().x, -bounds.getLocation().y);
            image = null;

            return bounds;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {

            Polygon drawPoly = new Polygon(c3Poly.xpoints, c3Poly.ypoints, c3Poly.npoints);
            drawPoly.translate(x, y);

            g.setColor(spriteColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.black);
            g.drawPolygon(drawPoly);
        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return c3Poly.contains(point.x - bounds.x, point.y - bounds.y);
        }

    }

    /**
     * Sprite and info for an attack. Does not actually use the image buffer as this can be horribly
     * inefficient for long diagonal lines. Appears as an arrow. Arrow becoming cut in half when two
     * Meks attacking each other.
     */
    private class AttackSprite extends Sprite {
        private ArrayList<AttackAction> attacks = new ArrayList<AttackAction>();

        private Point a;

        private Point t;

        private double an;

        private StraightArrowPolygon attackPoly;

        private Color attackColor;

        private int entityId;

        private int targetType;

        private int targetId;

        private String attackerDesc;

        private String targetDesc;

        ArrayList<String> weaponDescs = new ArrayList<String>();

        private final Entity ae;

        private final Targetable target;

        public AttackSprite(final AttackAction attack) {
            attacks.add(attack);
            entityId = attack.getEntityId();
            targetType = attack.getTargetType();
            targetId = attack.getTargetId();
            ae = game.getEntity(attack.getEntityId());
            target = game.getTarget(targetType, targetId);

            // color?
            attackColor = PlayerColors.getColor(ae.getOwner().getColorIndex());
            // angle of line connecting two hexes
            an = (ae.getPosition().radian(target.getPosition()) + (Math.PI * 1.5))
                    % (Math.PI * 2); // angle
            makePoly();

            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            // set names & stuff
            attackerDesc = ae.getDisplayName();
            targetDesc = target.getDisplayName();
            if (attack instanceof WeaponAttackAction) {
                addWeapon((WeaponAttackAction) attack);
            }
            if (attack instanceof KickAttackAction) {
                addWeapon((KickAttackAction) attack);
            }
            if (attack instanceof PunchAttackAction) {
                addWeapon((PunchAttackAction) attack);
            }
            if (attack instanceof PushAttackAction) {
                addWeapon((PushAttackAction) attack);
            }
            if (attack instanceof ClubAttackAction) {
                addWeapon((ClubAttackAction) attack);
            }
            if (attack instanceof ChargeAttackAction) {
                addWeapon((ChargeAttackAction) attack);
            }
            if (attack instanceof DfaAttackAction) {
                addWeapon((DfaAttackAction) attack);
            }
            if (attack instanceof ProtomechPhysicalAttackAction) {
                addWeapon((ProtomechPhysicalAttackAction) attack);
            }
            if (attack instanceof SearchlightAttackAction) {
                addWeapon((SearchlightAttackAction) attack);
            }

            // nullify image
            image = null;
        }

        private void makePoly() {
            // make a polygon
            a = getHexLocation(ae.getPosition());
            t = getHexLocation(target.getPosition());
            // OK, that is actually not good. I do not like hard coded figures.
            // HEX_W/2 - x distance in pixels from origin of hex bounding box to
            // the center of hex.
            // HEX_H/2 - y distance in pixels from origin of hex bounding box to
            // the center of hex.
            // 18 - is actually 36/2 - we do not want arrows to start and end
            // directly
            // in the centes of hex and hiding mek under.

            a.x = a.x + (int) (HEX_W / 2 * scale)
                    + (int) Math.round(Math.cos(an) * (int) (18 * scale));
            t.x = t.x + (int) (HEX_W / 2 * scale)
                    - (int) Math.round(Math.cos(an) * (int) (18 * scale));
            a.y = a.y + (int) (HEX_H / 2 * scale)
                    + (int) Math.round(Math.sin(an) * (int) (18 * scale));
            t.y = t.y + (int) (HEX_H / 2 * scale)
                    - (int) Math.round(Math.sin(an) * (int) (18 * scale));

            // Checking if given attack is mutual. In this case we building
            // halved arrow
            if (isMutualAttack()) {
                attackPoly = new StraightArrowPolygon(a, t, (int) (8 * scale), (int) (12 * scale),
                        true);
            } else {
                attackPoly = new StraightArrowPolygon(a, t, (int) (4 * scale), (int) (8 * scale),
                        false);
            }
        }

        @Override
        public Rectangle getBounds() {
            makePoly();
            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            return bounds;
        }

        /**
         * If we have build full arrow already with single attack and have got counter attack from
         * our target lately - lets change arrow to halved.
         */
        public void rebuildToHalvedPolygon() {
            attackPoly = new StraightArrowPolygon(a, t, (int) (8 * scale), (int) (12 * scale), true);
            // set bounds
            bounds = new Rectangle(attackPoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            attackPoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);
        }

        /**
         * Cheking if attack is mutual and changing target arrow to half-arrow
         */
        private boolean isMutualAttack() {
            for (AttackSprite sprite : attackSprites) {
                if ((sprite.getEntityId() == targetId) && (sprite.getTargetId() == entityId)) {
                    sprite.rebuildToHalvedPolygon();
                    return true;
                }
            }
            return false;
        }

        @Override
        public void prepare() {
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            Polygon drawPoly = new Polygon(attackPoly.xpoints, attackPoly.ypoints,
                    attackPoly.npoints);
            drawPoly.translate(x, y);

            g.setColor(attackColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.gray.darker());
            g.drawPolygon(drawPoly);
        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return attackPoly.contains(point.x - bounds.x, point.y - bounds.y);
        }

        public int getEntityId() {
            return entityId;
        }

        public int getTargetId() {
            return targetId;
        }

        /**
         * Adds a weapon to this attack
         */
        public void addWeapon(WeaponAttackAction attack) {
            final Entity entity = game.getEntity(attack.getEntityId());
            final WeaponType wtype = (WeaponType) entity.getEquipment(attack.getWeaponId())
                    .getType();
            final String roll = attack.toHit(game).getValueAsString();
            final String table = attack.toHit(game).getTableDesc();
            weaponDescs.add(wtype.getName()
                    + Messages.getString("BoardView1.needs") + roll + " " + table); //$NON-NLS-1$
        }

        public void addWeapon(KickAttackAction attack) {
            String bufer = ""; //$NON-NLS-1$
            String rollLeft = ""; //$NON-NLS-1$
            String rollRight = ""; //$NON-NLS-1$
            final int leg = attack.getLeg();
            switch (leg) {
            case KickAttackAction.BOTH:
                rollLeft = KickAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        KickAttackAction.LEFT).getValueAsString();
                rollRight = KickAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        KickAttackAction.RIGHT).getValueAsString();
                bufer = Messages.getString(
                        "BoardView1.kickBoth", new Object[] { rollLeft, rollRight }); //$NON-NLS-1$
                break;
            case KickAttackAction.LEFT:
                rollLeft = KickAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        KickAttackAction.LEFT).getValueAsString();
                bufer = Messages.getString("BoardView1.kickLeft", new Object[] { rollLeft }); //$NON-NLS-1$
                break;
            case KickAttackAction.RIGHT:
                rollRight = KickAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        KickAttackAction.RIGHT).getValueAsString();
                bufer = Messages.getString("BoardView1.kickRight", new Object[] { rollRight }); //$NON-NLS-1$
                break;
            }
            weaponDescs.add(bufer);
        }

        public void addWeapon(PunchAttackAction attack) {
            String bufer = ""; //$NON-NLS-1$
            String rollLeft = ""; //$NON-NLS-1$
            String rollRight = ""; //$NON-NLS-1$
            final int arm = attack.getArm();
            switch (arm) {
            case PunchAttackAction.BOTH:
                rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        PunchAttackAction.LEFT).getValueAsString();
                rollRight = PunchAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        PunchAttackAction.RIGHT).getValueAsString();
                bufer = Messages.getString(
                        "BoardView1.punchBoth", new Object[] { rollLeft, rollRight }); //$NON-NLS-1$
                break;
            case PunchAttackAction.LEFT:
                rollLeft = PunchAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        PunchAttackAction.LEFT).getValueAsString();
                bufer = Messages.getString("BoardView1.punchLeft", new Object[] { rollLeft }); //$NON-NLS-1$
                break;
            case PunchAttackAction.RIGHT:
                rollRight = PunchAttackAction.toHit(game, attack.getEntityId(),
                        game.getTarget(attack.getTargetType(), attack.getTargetId()),
                        PunchAttackAction.RIGHT).getValueAsString();
                bufer = Messages.getString("BoardView1.punchRight", new Object[] { rollRight }); //$NON-NLS-1$
                break;
            }
            weaponDescs.add(bufer);
        }

        public void addWeapon(PushAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString("BoardView1.push", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(ClubAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            final String club = attack.getClub().getName();
            weaponDescs.add(Messages.getString("BoardView1.hit", new Object[] { club, roll })); //$NON-NLS-1$
        }

        public void addWeapon(ChargeAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString("BoardView1.charge", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(DfaAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString("BoardView1.DFA", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(ProtomechPhysicalAttackAction attack) {
            final String roll = attack.toHit(game).getValueAsString();
            weaponDescs.add(Messages.getString("BoardView1.proto", new Object[] { roll })); //$NON-NLS-1$
        }

        public void addWeapon(SearchlightAttackAction attack) {
            weaponDescs.add(Messages.getString("BoardView1.Searchlight"));
        }

        @Override
        public String[] getTooltip() {
            String[] tipStrings = new String[1 + weaponDescs.size()];
            int tip = 1;
            tipStrings[0] = attackerDesc
                    + " " + Messages.getString("BoardView1.on") + " " + targetDesc; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            for (Iterator<String> i = weaponDescs.iterator(); i.hasNext();) {
                tipStrings[tip++] = i.next();
            }
            return tipStrings;
        }
    }

    /**
     * Sprite and info for movement vector (AT2 advanced movement). Does not actually use the image
     * buffer as this can be horribly inefficient for long diagonal lines.
     *
     * Appears as an arrow pointing to the hex this entity will move to based on current movement
     * vectors. TODO: Different color depending upon whether entity has already moved this turn
     *
     */
    private class MovementSprite extends Sprite {
        private Point a;

        private Point t;

        private double an;

        private StraightArrowPolygon movePoly;

        private Color moveColor;

        // private MovementVector mv;
        private int[] vectors;

        private Coords start;

        private Coords end;

        private Entity en;

        private int vel;

        public MovementSprite(Entity e, int[] v, Color col, boolean isCurrent) {
            // this.mv = en.getMV();

            en = e;
            vectors = v;// en.getVectors();
            // get the starting and ending position
            start = en.getPosition();
            end = Compute.getFinalPosition(start, vectors);

            // what is the velocity
            vel = 0;
            for (int element : v) {
                vel += element;
            }

            // color?
            // player colors
            moveColor = PlayerColors.getColor(en.getOwner().getColorIndex());
            // TODO: Its not going transparent. Oh well, it is a minor issue at the moment
            /*
             * if(isCurrent) { int colour = col.getRGB(); int transparency =
             * GUIPreferences.getInstance().getInt(GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
             * moveColor = new Color(colour | (transparency << 24), true); }
             */
            // red if offboard
            if (!game.getBoard().contains(end)) {
                int colour = 0xff0000; // red
                int transparency = GUIPreferences.getInstance().getInt(
                        GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
                moveColor = new Color(colour | (transparency << 24), true);
            }
            // dark gray if done
            if (en.isDone()) {
                int colour = 0x696969; // gray
                int transparency = GUIPreferences.getInstance().getInt(
                        GUIPreferences.ADVANCED_ATTACK_ARROW_TRANSPARENCY);
                moveColor = new Color(colour | (transparency << 24), true);
            }

            // moveColor = PlayerColors.getColor(en.getOwner().getColorIndex());
            // angle of line connecting two hexes
            an = (start.radian(end) + (Math.PI * 1.5)) % (Math.PI * 2); // angle
            makePoly();

            // set bounds
            bounds = new Rectangle(movePoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            movePoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            // nullify image
            image = null;
        }

        private void makePoly() {
            // make a polygon
            a = getHexLocation(start);
            t = getHexLocation(end);
            // OK, that is actually not good. I do not like hard coded figures.
            // HEX_W/2 - x distance in pixels from origin of hex bounding box to the center of hex.
            // HEX_H/2 - y distance in pixels from origin of hex bounding box to the center of hex.
            // 18 - is actually 36/2 - we do not want arrows to start and end directly
            // in the centes of hex and hiding mek under.

            a.x = a.x + (int) (HEX_W / 2 * scale)
                    + (int) Math.round(Math.cos(an) * (int) (18 * scale));
            t.x = t.x + (int) (HEX_W / 2 * scale)
                    - (int) Math.round(Math.cos(an) * (int) (18 * scale));
            a.y = a.y + (int) (HEX_H / 2 * scale)
                    + (int) Math.round(Math.sin(an) * (int) (18 * scale));
            t.y = t.y + (int) (HEX_H / 2 * scale)
                    - (int) Math.round(Math.sin(an) * (int) (18 * scale));
            movePoly = new StraightArrowPolygon(a, t, (int) (4 * scale), (int) (8 * scale), false);
        }

        @Override
        public Rectangle getBounds() {
            makePoly();
            // set bounds
            bounds = new Rectangle(movePoly.getBounds());
            bounds.setSize(bounds.getSize().width + 1, bounds.getSize().height + 1);
            // move poly to upper right of image
            movePoly.translate(-bounds.getLocation().x, -bounds.getLocation().y);

            return bounds;
        }

        @Override
        public void prepare() {

        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void drawOnto(Graphics g, int x, int y, ImageObserver observer) {
            // don't draw anything if the unit has no velocity

            if (vel == 0) {
                return;
            }

            Polygon drawPoly = new Polygon(movePoly.xpoints, movePoly.ypoints, movePoly.npoints);
            drawPoly.translate(x, y);

            g.setColor(moveColor);
            g.fillPolygon(drawPoly);
            g.setColor(Color.gray.darker());
            g.drawPolygon(drawPoly);

        }

        /**
         * Return true if the point is inside our polygon
         */
        @Override
        public boolean isInside(Point point) {
            return movePoly.contains(point.x - bounds.x, point.y - bounds.y);
        }

        /*
         * public String[] getTooltip() { String[] tipStrings = new String[1 + weaponDescs.size()];
         * int tip = 1; tipStrings[0] = attackerDesc + " "+Messages.getString("BoardView1.on")+" " +
         * targetDesc; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ for (Iterator<String> i =
         * weaponDescs.iterator(); i.hasNext();) { tipStrings[tip++] = i.next(); } return
         * tipStrings; }
         */
    }

    /**
     * Determine if the tile manager's images have been loaded.
     *
     * @return <code>true</code> if all images have been loaded. <code>false</code> if more need
     *         to be loaded.
     */
    public boolean isTileImagesLoaded() {
        return tileManager.isLoaded();
    }

    public void setUseLOSTool(boolean use) {
        useLOSTool = use;
    }

    public TilesetManager getTilesetManager() {
        return tileManager;
    }

    /**
     * @param lastCursor
     *            The lastCursor to set.
     */
    public void setLastCursor(Coords lastCursor) {
        this.lastCursor = lastCursor;
    }

    /**
     * @return Returns the lastCursor.
     */
    public Coords getLastCursor() {
        return lastCursor;
    }

    /**
     * @param highlighted
     *            The highlighted to set.
     */
    public void setHighlighted(Coords highlighted) {
        this.highlighted = highlighted;
    }

    /**
     * @return Returns the highlighted.
     */
    public Coords getHighlighted() {
        return highlighted;
    }

    /**
     * @param selected
     *            The selected to set.
     */
    public void setSelected(Coords selected) {
        this.selected = selected;
    }

    /**
     * @return Returns the selected.
     */
    public Coords getSelected() {
        return selected;
    }

    /**
     * @param firstLOS
     *            The firstLOS to set.
     */
    public void setFirstLOS(Coords firstLOS) {
        this.firstLOS = firstLOS;
    }

    /**
     * @return Returns the firstLOS.
     */
    public Coords getFirstLOS() {
        return firstLOS;
    }

    /**
     * Determines if this Board contains the Coords, and if so, "selects" that Coords.
     *
     * @param coords
     *            the Coords.
     */
    public void select(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            setSelected(coords);
            moveCursor(selectedSprite, coords);
            moveCursor(firstLOSSprite, null);
            moveCursor(secondLOSSprite, null);
            processBoardViewEvent(new BoardViewEvent(this, coords, null,
                    BoardViewEvent.BOARD_HEX_SELECTED, 0));
        }
    }

    /**
     * "Selects" the specified Coords.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void select(int x, int y) {
        select(new Coords(x, y));
    }

    /**
     * Determines if this Board contains the Coords, and if so, highlights that Coords.
     *
     * @param coords
     *            the Coords.
     */
    public void highlight(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            setHighlighted(coords);
            moveCursor(highlightSprite, coords);
            moveCursor(firstLOSSprite, null);
            moveCursor(secondLOSSprite, null);
            processBoardViewEvent(new BoardViewEvent(this, coords, null,
                    BoardViewEvent.BOARD_HEX_HIGHLIGHTED, 0));
        }
    }

    /**
     * Highlights the specified Coords.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void highlight(int x, int y) {
        highlight(new Coords(x, y));
    }

    /**
     * Determines if this Board contains the Coords, and if so, "cursors" that Coords.
     *
     * @param coords
     *            the Coords.
     */
    public void cursor(Coords coords) {
        if ((coords == null) || game.getBoard().contains(coords)) {
            if ((getLastCursor() == null) || (coords == null) || !coords.equals(getLastCursor())) {
                setLastCursor(coords);
                moveCursor(cursorSprite, coords);
                moveCursor(firstLOSSprite, null);
                moveCursor(secondLOSSprite, null);
                processBoardViewEvent(new BoardViewEvent(this, coords, null,
                        BoardViewEvent.BOARD_HEX_CURSOR, 0));
            } else {
                setLastCursor(coords);
            }
        }
    }

    /**
     * "Cursors" the specified Coords.
     *
     * @param x
     *            the x coordinate.
     * @param y
     *            the y coordinate.
     */
    public void cursor(int x, int y) {
        cursor(new Coords(x, y));
    }

    public void checkLOS(Coords c) {
        if ((c == null) || game.getBoard().contains(c)) {
            if (getFirstLOS() == null) {
                setFirstLOS(c);
                firstLOSHex(c);
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_FIRST_LOS_HEX, 0));
            } else {
                secondLOSHex(c, getFirstLOS());
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_SECOND_LOS_HEX, 0));
                setFirstLOS(null);
            }
        }
    }

    /**
     * Determines if this Board contains the (x, y) Coords, and if so, notifies listeners about the
     * specified mouse action.
     */
    public void mouseAction(int x, int y, int mtype, int modifiers) {
        if (game.getBoard().contains(x, y)) {
            Coords c = new Coords(x, y);
            switch (mtype) {
            case BOARD_HEX_CLICK:
                if ((modifiers & java.awt.event.InputEvent.CTRL_MASK) != 0) {
                    checkLOS(c);
                } else {
                    processBoardViewEvent(new BoardViewEvent(this, c, null,
                            BoardViewEvent.BOARD_HEX_CLICKED, modifiers));
                }
                break;
            case BOARD_HEX_DOUBLECLICK:
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_HEX_DOUBLECLICKED, modifiers));
                break;
            case BOARD_HEX_DRAG:
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_HEX_DRAGGED, modifiers));
                break;
            case BOARD_HEX_POPUP:
                processBoardViewEvent(new BoardViewEvent(this, c, null,
                        BoardViewEvent.BOARD_HEX_POPUP, modifiers));
                break;
            }
        }
    }

    /**
     * Notifies listeners about the specified mouse action.
     *
     * @param coords
     *            the Coords.
     */
    public void mouseAction(Coords coords, int mtype, int modifiers) {
        mouseAction(coords.x, coords.y, mtype, modifiers);
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardNewBoard(megamek.common.BoardEvent)
     */
    public void boardNewBoard(BoardEvent b) {
        updateBoard();
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public synchronized void boardChangedHex(BoardEvent b) {
        IHex hex = game.getBoard().getHex(b.getCoords());
        tileManager.clearHex(hex);
        tileManager.waitForHex(hex);
        repaint();
    }

    /*
     * (non-Javadoc)
     *
     * @see megamek.common.BoardListener#boardChangedHex(megamek.common.BoardEvent)
     */
    public synchronized void boardChangedAllHexes(BoardEvent b) {
        tileManager.loadAllHexes();
        repaint();
    }

    private GameListener gameListener = new GameListenerAdapter() {

        @Override
        public void gameEntityNew(GameEntityNewEvent e) {
            updateEcmList();
            redrawAllEntities();
            if (game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
        }

        @Override
        public void gameEntityRemove(GameEntityRemoveEvent e) {
            updateEcmList();
            redrawAllEntities();
            if (game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
        }

        @Override
        public void gameEntityChange(GameEntityChangeEvent e) {
            Vector<UnitLocation> mp = e.getMovePath();
            updateEcmList();
            if (e.getEntity().hasActiveECM()) {
                // this might disrupt c3/c3i lines, so redraw all
                redrawAllEntities();
            }
            if (game.getPhase() == IGame.Phase.PHASE_MOVEMENT) {
                refreshMoveVectors();
            }
            if ((mp != null) && (mp.size() > 0) && GUIPreferences.getInstance().getShowMoveStep()) {
                addMovingUnit(e.getEntity(), mp);
            } else {
                redrawEntity(e.getEntity());
            }
        }

        @Override
        public void gameNewAction(GameNewActionEvent e) {
            EntityAction ea = e.getAction();
            if (ea instanceof AttackAction) {
                addAttack((AttackAction) ea);
            }
        }

        @Override
        public void gameBoardNew(GameBoardNewEvent e) {
            IBoard b = e.getOldBoard();
            if (b != null) {
                b.removeBoardListener(BoardView1.this);
            }
            b = e.getNewBoard();
            if (b != null) {
                b.addBoardListener(BoardView1.this);
            }
            updateBoard();
        }

        @Override
        public void gameBoardChanged(GameBoardChangeEvent e) {
            boardChanged();
        }

        @Override
        public void gamePhaseChange(GamePhaseChangeEvent e) {
            refreshAttacks();
            switch (e.getNewPhase()) {
            case PHASE_MOVEMENT:
                refreshMoveVectors();
            case PHASE_FIRING:
                clearAllMoveVectors();
            case PHASE_PHYSICAL:
                refreshAttacks();
                break;
            case PHASE_INITIATIVE:
                clearAllAttacks();
                break;
            case PHASE_END:
            case PHASE_VICTORY:
                clearSprites();
            default:
            }
        }
    };

    synchronized void boardChanged() {
        redrawAllEntities();
    }

    void clearSprites() {
        pathSprites.clear();
        attackSprites.clear();
        c3Sprites.clear();
        movementSprites.clear();

    }

    protected synchronized void updateBoard() {
        updateBoardSize();
        redrawAllEntities();
    }

    /**
     * the old redrawworker converted to a runnable which is called now and then from the event
     * thread
     */
    protected class RedrawWorker implements Runnable {

        protected long lastTime = System.currentTimeMillis();

        protected long currentTime = System.currentTimeMillis();

        public void run() {
            currentTime = System.currentTimeMillis();
            if (isShowing()) {
                boolean redraw = false;
                for (int i = 0; i < displayables.size(); i++) {
                    IDisplayable disp = displayables.get(i);
                    if (!disp.isSliding()) {
                        disp.setIdleTime(currentTime - lastTime, true);
                    } else {
                        redraw = redraw || disp.slide();
                    }
                }
                redraw = redraw || doMoveUnits(currentTime - lastTime);
                if (redraw) {
                    repaint();
                }
            }
            lastTime = currentTime;
        }
    }

    public synchronized void WeaponSelected(MechDisplayEvent b) {
        selectedEntity = b.getEntity();
        selectedWeapon = b.getEquip();
        repaint();
    }

    private class EcmBubble extends Coords {
        /**
         *
         */
        private static final long serialVersionUID = 3304636458460529324L;

        int range;
        int tint;
        int direction;

        public EcmBubble(Coords c, int range, int tint) {
            super(c);
            this.range = range;
            this.tint = tint;
            direction = -1;
        }

        public EcmBubble(Coords c, int range, int tint, int direction) {
            super(c);
            this.range = range;
            this.tint = tint;
            this.direction = direction;
        }

    }

    // This is expensive, so precalculate when entity changes
    public void updateEcmList() {
        ArrayList<EcmBubble> list = new ArrayList<EcmBubble>();
        for (Enumeration<Entity> e = game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            int range = ent.getECMRange();
            boolean deployed = ent.isDeployed();
            boolean offboard = ent.isOffBoard();
            if((entPos == null) && (ent.getTransportId() != Entity.NONE)) {
                Entity carrier = game.getEntity(ent.getTransportId());
                if((null != carrier) && carrier.loadedUnitsHaveActiveECM()) {
                    entPos = carrier.getPosition();
                    deployed = carrier.isDeployed();
                    offboard = carrier.isOffBoard();
                }
            }
            if ((entPos == null) || !deployed || offboard) {
                continue;
            }
            if (range != Entity.NONE) {
                int tint = PlayerColors.getColorRGB(ent.getOwner().getColorIndex());
                list.add(new EcmBubble(entPos, range, tint));
            }
            if(game.getBoard().inSpace()) {
                //then BAP is also ECCM so it needs a bubble
                range = ent.getBAPRange();
                int direction = -1;
                if (range != Entity.NONE) {
                    if(range > 6) {
                        direction = ent.getFacing();
                    }
                    int tint = PlayerColors.getColorRGB(ent.getOwner()
                            .getColorIndex());
                    list.add(new EcmBubble(entPos, range, tint, direction));
                }
            }
        }
        HashMap<Coords, Integer> table = new HashMap<Coords, Integer>();
        for (EcmBubble b : list) {
            Integer col = new Integer(b.tint);
            for (int x = -b.range; x <= b.range; x++) {
                for (int y = -b.range; y <= b.range; y++) {
                    Coords c = new Coords(x + b.x, y + b.y);
                    // clip rectangle to hexagon
                    if ((b.distance(c) <= b.range) && ((b.direction == -1) || Compute.isInArc(b, b.direction, c, Compute.ARC_NOSE))) {
                        Integer tint = table.get(c);
                        if (tint == null) {
                            table.put(c, col);
                        } else if (tint.intValue() != b.tint) {
                            int red1 = (tint.intValue() >> 16) & 0xff;
                            int green1 = (tint.intValue() >> 8) & 0xff;
                            int blue1 = tint.intValue() & 0xff;
                            int red2 = (b.tint >> 16) & 0xff;
                            int green2 = (b.tint >> 8) & 0xff;
                            int blue2 = b.tint & 0xff;
                            red1 = (red1 + red2) / 2;
                            green1 = (green1 + green2) / 2;
                            blue1 = (blue1 + blue2) / 2;
                            table.put(c, new Integer((red1 << 16) | (green1 << 8) | blue1));
                        }
                    }
                }
            }
        }
        synchronized (this) {
            ecmHexes = table;
        }
        repaint();
    }

    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    public int getScrollableBlockIncrement(Rectangle arg0, int arg1, int arg2) {
        final Dimension size = scrollpane.getViewport().getSize();
        if (arg1 == SwingConstants.VERTICAL) {
            return size.height;
        }
        return size.width;
    }

    public boolean getScrollableTracksViewportHeight() {
        return false;
    }

    public boolean getScrollableTracksViewportWidth() {
        return false;
    }

    public int getScrollableUnitIncrement(Rectangle arg0, int arg1, int arg2) {
        if (arg1 == SwingConstants.VERTICAL) {
            return (int) (scale * HEX_H / 2.0);
        }
        return (int) (scale * HEX_W / 2.0);
    }

    @Override
    public Dimension getPreferredSize() {
        return boardSize;
    }

    /**
     * Have the player select an Entity from the entities at the given coords.
     *
     * @param pos -
     *            the <code>Coords</code> containing targets.
     */
    private Entity chooseEntity(Coords pos) {

        // Assume that we have *no* choice.
        Entity choice = null;

        // Get the available choices.
        Enumeration<Entity> choices = game.getEntities(pos);

        // Convert the choices into a List of targets.
        Vector<Entity> entities = new Vector<Entity>();
        while (choices.hasMoreElements()) {
            entities.addElement(choices.nextElement());
        }

        // Do we have a single choice?
        if (entities.size() == 1) {
            // Return that choice.
            choice = entities.elementAt(0);
        }

        // If we have multiple choices, display a selection dialog.
        else if (entities.size() > 1) {
            String[] names = new String[entities.size()];
            for (int loop = 0; loop < names.length; loop++) {
                names[loop] = entities.elementAt(loop).getDisplayName();
            }
            SingleChoiceDialog choiceDialog = new SingleChoiceDialog(null,
                    Messages.getString("BoardView1.ChooseEntityDialog.title"), //$NON-NLS-1$
                    Messages
                            .getString(
                                    "BoardView1.ChooseEntityDialog.message", new Object[] { pos.getBoardNum() }), //$NON-NLS-1$
                    names);
            choiceDialog.setVisible(true);
            if (choiceDialog.getAnswer() == true) {
                choice = entities.elementAt(choiceDialog.getChoice());
            }
        } // End have-choices

        // Return the chosen unit.
        return choice;
    }

    /**
     * The text to be displayed when the mouse is at a certain point TODO: just use a
     * StringBuffer... This is copied from the AWT version
     */
    @Override
    public String getToolTipText(MouseEvent e) {

        int stringsSize = 0;
        IHex mhex = null;
        Point point = e.getPoint();

        // first, we have to determine how much text we are going to have
        // are we on a hex?
        final Coords mcoords = getCoordsAt(point);
        if (GUIPreferences.getInstance().getShowMapHexPopup() && game.getBoard().contains(mcoords)) {
            mhex = game.getBoard().getHex(mcoords);
            stringsSize += 1;
        }

        // check if it's on any entities
        for (EntitySprite eSprite : entitySprites) {
            if (eSprite.isInside(point)) {
                stringsSize += 3;
            }
        }

        // check if it's on any attacks
        for (AttackSprite aSprite : attackSprites) {
            if (aSprite.isInside(point)) {
                stringsSize += 1 + aSprite.weaponDescs.size();
            }
        }

        // If the hex contains a building, make more space.
        // Also if it contains other displayable terrain.
        if (mhex != null) {
            stringsSize += mhex.displayableTerrainsPresent();
            if (mhex.containsTerrain(Terrains.BUILDING)) {
                stringsSize++;
            }
            if (mhex.containsTerrain(Terrains.FUEL_TANK)) {
                stringsSize++;
            }
            if (mhex.containsTerrain(Terrains.BRIDGE)) {
                stringsSize++;
            }
        }

        stringsSize += game.getNbrMinefields(mcoords);

        // Artillery
        final ArrayList<ArtilleryAttackAction> artilleryAttacks = getArtilleryAttacksAtLocation(mcoords);
        stringsSize += artilleryAttacks.size();

        // Artillery fire adjustment
        final Mounted curWeapon = getSelectedArtilleryWeapon();
        if (curWeapon != null) {
            stringsSize++;
        }

        // if the size is zip, you must a'quit
        if (stringsSize == 0) {
            return null;
        }

        // now we can allocate an array of strings
        String[] strings = new String[stringsSize];
        int stringsIndex = 0;

        // are we on a hex?
        if (mhex != null) {
            strings[stringsIndex] = Messages.getString("BoardView1.Hex") + mcoords.getBoardNum() //$NON-NLS-1$
                    + Messages.getString("BoardView1.level") + mhex.getElevation(); //$NON-NLS-1$
            stringsIndex += 1;

            //cycle through the terrains and report types found
            //this will skip buildings and other constructed units
            for(int i=0;i < Terrains.SIZE; i++) {
                if(mhex.containsTerrain(i)) {
                    int tf = mhex.getTerrain(i).getTerrainFactor();
                    int ttl = mhex.getTerrain(i).getLevel();
                    String name = Terrains.getDisplayName(i, ttl);
                    if(tf > 0) {
                        name = name + " (" + tf + ")";
                    }
                    if(null != name) {
                        strings[stringsIndex] = name;
                        stringsIndex += 1;
                    }
                }
            }

            // Do we have a building?
            if (mhex.containsTerrain(Terrains.FUEL_TANK)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages.getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.FUEL_TANK_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }
            if (mhex.containsTerrain(Terrains.BUILDING)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages.getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.BLDG_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }

            // Do we have a bridge?
            if (mhex.containsTerrain(Terrains.BRIDGE)) {
                // Get the building.
                Building bldg = game.getBoard().getBuildingAt(mcoords);
                StringBuffer buf = new StringBuffer(Messages.getString("BoardView1.Height")); //$NON-NLS-1$
                // Each hex of a building has its own elevation.
                buf.append(mhex.terrainLevel(Terrains.BRIDGE_ELEV));
                buf.append(" "); //$NON-NLS-1$
                buf.append(bldg.toString());
                buf.append(Messages.getString("BoardView1.CF")); //$NON-NLS-1$
                buf.append(bldg.getCurrentCF(mcoords));
                strings[stringsIndex] = buf.toString();
                stringsIndex += 1;
            }

            if (game.containsMinefield(mcoords)) {
                Vector<Minefield> minefields = game.getMinefields(mcoords);
                for (int i = 0; i < minefields.size(); i++) {
                    Minefield mf = minefields.elementAt(i);
                    String owner = " (" + game.getPlayer(mf.getPlayerId()).getName() + ")"; //$NON-NLS-1$ //$NON-NLS-2$

                    switch (mf.getType()) {
                    case (Minefield.TYPE_CONVENTIONAL):
                        strings[stringsIndex] = mf.getName()
                                + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                        break;
                    case (Minefield.TYPE_COMMAND_DETONATED):
                        strings[stringsIndex] = mf.getName()
                                + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                        break;
                    case (Minefield.TYPE_VIBRABOMB):
                        if (mf.getPlayerId() == localPlayer.getId()) {
                            strings[stringsIndex] = mf.getName()
                                    + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + "(" + mf.getSetting() + ") " + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        } else {
                            strings[stringsIndex] = mf.getName()
                                    + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + " " + owner; //$NON-NLS-1$ //$NON-NLS-2$
                        }
                        break;
                    case (Minefield.TYPE_ACTIVE):
                        strings[stringsIndex] = mf.getName()
                                + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        break;
                    case (Minefield.TYPE_INFERNO):
                        strings[stringsIndex] = mf.getName()
                                + Messages.getString("BoardView1.minefield") + "(" + mf.getDensity() + ")" + owner; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        break;
                    }
                    stringsIndex++;
                }
            }
        }
        // check if it's on any entities
        for (EntitySprite eSprite : entitySprites) {
            if (eSprite.isInside(point)) {
                final String[] entityStrings = eSprite.getTooltip();
                System.arraycopy(entityStrings, 0, strings, stringsIndex, entityStrings.length);
                stringsIndex += entityStrings.length;
            }
        }

        // check if it's on any attacks
        for (AttackSprite aSprite : attackSprites) {
            if (aSprite.isInside(point)) {
                final String[] attackStrings = aSprite.getTooltip();
                System.arraycopy(attackStrings, 0, strings, stringsIndex, attackStrings.length);
                stringsIndex += 1 + aSprite.weaponDescs.size();
            }
        }

        // check artillery attacks
        for (ArtilleryAttackAction aaa : artilleryAttacks) {
            final Entity ae = game.getEntity(aaa.getEntityId());
            String s = null;
            if (ae != null) {
                if (aaa.getWeaponId() > -1) {
                    Mounted weap = ae.getEquipment(aaa.getWeaponId());
                    s = weap.getName();
                    if (aaa.getAmmoId() > -1) {
                        Mounted ammo = ae.getEquipment(aaa.getAmmoId());
                        s += "(" + ammo.getName() + ")";
                    }
                }
            }
            if (s == null) {
                s = Messages.getString("BoardView1.Artillery");
            }
            strings[stringsIndex++] = Messages.getString("BoardView1.ArtilleryAttack",
                    new Object[] { s, new Integer(aaa.turnsTilHit),
                            aaa.toHit(game).getValueAsString() });
        }

        // check artillery fire adjustment
        if ((curWeapon != null) && (selectedEntity != null)) {
            // process targetted hexes
            int amod = 0;
            // Check the predesignated hexes
            if (selectedEntity.getOwner().getArtyAutoHitHexes().contains(mcoords)) {
                amod = TargetRoll.AUTOMATIC_SUCCESS;
            } else {
                amod = selectedEntity.aTracker.getModifier(curWeapon, mcoords);
            }

            if (amod == TargetRoll.AUTOMATIC_SUCCESS) {
                strings[stringsIndex++] = Messages.getString("BoardView1.ArtilleryAutohit");
            } else {
                strings[stringsIndex++] = Messages.getString("BoardView1.ArtilleryAdjustment",
                        new Object[] { new Integer(amod) });
            }
        }

        StringBuffer sb = new StringBuffer();
        sb.append("<html>");
        for (String str : strings) {
            sb.append(str);
            sb.append("<br>");
        }
        sb.append("</html>");
        return sb.toString();
    }

    private ArrayList<ArtilleryAttackAction> getArtilleryAttacksAtLocation(Coords c) {
        ArrayList<ArtilleryAttackAction> v = new ArrayList<ArtilleryAttackAction>();
        for (Enumeration<ArtilleryAttackAction> attacks = game.getArtilleryAttacks(); attacks
                .hasMoreElements();) {
            ArtilleryAttackAction a = attacks.nextElement();
            if (a.getTarget(game).getPosition().equals(c)) {
                v.add(a);
            }
        }
        return v;
    }

    public Component getComponent() {
        if (scrollpane != null) {
            return scrollpane;
        }

        // Place the board viewer in a set of scrollbars.
        scrollpane = new JScrollPane(this);
        scrollpane.setLayout(new ScrollPaneLayout());
        // we need to use the simple scroll mode because otherwise the
        // IDisplayables that are drawn in fixed positions in the viewport
        // leave artifacts when scrolling
        scrollpane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);

        return scrollpane;
    }

    public void refreshDisplayables() {
        repaint();
    }

    public void showPopup(Object popup, Coords c) {
        Point p = getHexLocation(c);
        p.x += (int) (HEX_WC * scale) - scrollpane.getX();
        p.y += (int) (HEX_H * scale / 2) - scrollpane.getY();
        if (((JPopupMenu)popup).getParent() == null) {
            add((JPopupMenu)popup);
        }
        ((JPopupMenu)popup).show(this, p.x, p.y);
    }

    public void refreshMinefields() {
        repaint();
    }

    public void zoomIn() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void zoomOut() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public void hideTooltip() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

}
TOP

Related Classes of megamek.client.ui.swing.BoardView1$EntitySprite

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.