/*
 * MegaMek -
 * Copyright (C) 2000,2001,2002,2003,2004,2005 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.common;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import megamek.common.actions.BAVibroClawAttackAction;
import megamek.common.actions.BreakGrappleAttackAction;
import megamek.common.actions.BrushOffAttackAction;
import megamek.common.actions.ClubAttackAction;
import megamek.common.actions.EntityAction;
import megamek.common.actions.GrappleAttackAction;
import megamek.common.actions.JumpJetAttackAction;
import megamek.common.actions.KickAttackAction;
import megamek.common.actions.LayExplosivesAttackAction;
import megamek.common.actions.ProtomechPhysicalAttackAction;
import megamek.common.actions.PunchAttackAction;
import megamek.common.actions.PushAttackAction;
import megamek.common.actions.ThrashAttackAction;
import megamek.common.actions.TripAttackAction;
import megamek.common.actions.WeaponAttackAction;
import megamek.common.weapons.BayWeapon;

/**
 * The compute class is designed to provide static methods for mechs and other
 * entities moving, firing, etc.
 */
public class Compute {

    public static final int ARC_360 = 0;
    public static final int ARC_FORWARD = 1;
    public static final int ARC_LEFTARM = 2;
    public static final int ARC_RIGHTARM = 3;
    public static final int ARC_REAR = 4;
    public static final int ARC_LEFTSIDE = 5;
    public static final int ARC_RIGHTSIDE = 6;
    public static final int ARC_MAINGUN = 7;
    public static final int ARC_NORTH = 8;
    public static final int ARC_EAST = 9;
    public static final int ARC_WEST = 10;
    public static final int ARC_NOSE = 11;
    public static final int ARC_LWING = 12;
    public static final int ARC_RWING = 13;
    public static final int ARC_LWINGA = 14;
    public static final int ARC_RWINGA = 15;
    public static final int ARC_LEFTSIDE_SPHERE = 16;
    public static final int ARC_RIGHTSIDE_SPHERE = 17;
    public static final int ARC_LEFTSIDEA_SPHERE = 18;
    public static final int ARC_RIGHTSIDEA_SPHERE= 19;
    public static final int ARC_LEFT_BROADSIDE = 20;
    public static final int ARC_RIGHT_BROADSIDE =21;
    public static final int ARC_AFT = 22;
    public static final int ARC_LEFT_SPHERE_GROUND = 23;
    public static final int ARC_RIGHT_SPHERE_GROUND =24;
    public static final int ARC_TURRET = 25;

    public static final int TYPE_IS = 0;
    public static final int TYPE_CLAN = 1;
    public static final int TYPE_MD = 2;

    public static final int LEVEL_GREEN = 0;
    public static final int LEVEL_REGULAR = 1;
    public static final int LEVEL_VETERAN = 2;
    public static final int LEVEL_ELITE = 3;

    public static final int METHOD_TW = 0;
    public static final int METHOD_TAHARQA = 1;
    public static final int METHOD_CONSTANT = 2;

    public static final int WEAPON_DIRECT_FIRE = 0;
    public static final int WEAPON_CLUSTER_BALLISTIC = 1;
    public static final int WEAPON_PULSE = 2;
    public static final int WEAPON_CLUSTER_MISSILE = 3;
    public static final int WEAPON_CLUSTER_MISSILE_1D6 = 4;
    public static final int WEAPON_CLUSTER_MISSILE_2D6 = 5;
    public static final int WEAPON_CLUSTER_MISSILE_3D6 = 6;

    private static final int[][] skillLevels = new int[][] {
        { 7, 6, 5, 4, 4, 3, 2, 1, 0 },
        { 7, 7, 6, 6, 5, 4, 3, 2, 1 } };

    private static MMRandom random = MMRandom.generate(MMRandom.R_DEFAULT);

    private static final int[][] clusterHitsTable = new int[][] {
        { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
        { 2, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2 },
        { 3, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3 },
        { 4, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4 },
        { 5, 1, 2, 2, 3, 3, 3, 3, 4, 4, 5, 5 },
        { 6, 2, 2, 3, 3, 4, 4, 4, 5, 5, 6, 6 },
        { 7, 2, 2, 3, 4, 4, 4, 4, 6, 6, 7, 7 },
        { 8, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 8 },
        { 9, 3, 3, 4, 5, 5, 5, 5, 7, 7, 9, 9 },
        { 10, 3, 3, 4, 6, 6, 6, 6, 8, 8, 10, 10 },
        { 11, 4, 4, 5, 7, 7, 7, 7, 9, 9, 11, 11 },
        { 12, 4, 4, 5, 8, 8, 8, 8, 10, 10, 12, 12 },
        { 13, 4, 4, 5, 8, 8, 8, 8, 11, 11, 13, 13 },
        { 14, 5, 5, 6, 9, 9, 9, 9, 11, 11, 14, 14 },
        { 15, 5, 5, 6, 9, 9, 9, 9, 12, 12, 15, 15 },
        { 16, 5, 5, 7, 10, 10, 10, 10, 13, 13, 16, 16 },
        { 17, 5, 5, 7, 10, 10, 10, 10, 14, 14, 17, 17 },
        { 18, 6, 6, 8, 11, 11, 11, 11, 14, 14, 18, 18 },
        { 19, 6, 6, 8, 11, 11, 11, 11, 15, 15, 19, 19 },
        { 20, 6, 6, 9, 12, 12, 12, 12, 16, 16, 20, 20 },
        { 21, 7, 7, 9, 13, 13, 13, 13, 17, 17, 21, 21 },
        { 22, 7, 7, 9, 14, 14, 14, 14, 18, 18, 22, 22 },
        { 23, 7, 7, 10, 15, 15, 15, 15, 19, 19, 23, 23 },
        { 24, 8, 8, 10, 16, 16, 16, 16, 20, 20, 24, 24 },
        { 25, 8, 8, 10, 16, 16, 16, 16, 21, 21, 25, 25 },
        { 26, 9, 9, 11, 17, 17, 17, 17, 21, 21, 26, 26 },
        { 27, 9, 9, 11, 17, 17, 17, 17, 22, 22, 27, 27 },
        { 28, 9, 9, 11, 17, 17, 17, 17, 23, 23, 28, 28 },
        { 29, 10, 10, 12, 18, 18, 18, 18, 23, 23, 29, 29 },
        { 30, 10, 10, 12, 18, 18, 18, 18, 24, 24, 30, 30 },
        { 40, 12, 12, 18, 24, 24, 24, 24, 32, 32, 40, 40 } };

    /** Wrapper to random#d6(n) */
    public static int d6(int dice) {
        Roll roll = random.d6(dice);
        return roll.getIntValue();
    }

    /** Wrapper to random#d6() */
    public static int d6() {
        Roll roll = random.d6();
        return roll.getIntValue();
    }

    /** Wrapper to random#randomInt(n) */
    public static int randomInt(int maxValue) {
        Roll roll = new MMRoll(random, maxValue);
        return roll.getIntValue();
    }

    /**
     * Sets the RNG to the desired type
     */
    public static void setRNG(int type) {
        random = MMRandom.generate(type);
    }

    /**
     * Returns the odds that a certain number or above will be rolled on 2d6.
     */
    public static double oddsAbove(int n) {
        if (n <= 2) {
            return 100.0;
        } else if (n > 12) {
            return 0;
        }
        final double[] odds = { 100.0, 100.0, 100.0, 97.2, 91.6, 83.3, 72.2, 58.3, 41.6, 27.7, 16.6, 8.3, 2.78, 0 };
        return odds[n];
    }

    /**
     * Returns an entity if the specified entity would cause a stacking
     * violation entering a hex, or returns null if it would not. The returned
     * entity is the entity causing the violation.
     */
    public static Entity stackingViolation(IGame game, int enteringId, Coords coords) {
        Entity entering = game.getEntity(enteringId);
        return Compute.stackingViolation(game, entering, coords, null);
    }

    /**
     * When compiling an unloading step, both the transporter and the unloaded
     * unit probably occupy some other position on the board.
     */
    public static Entity stackingViolation(IGame game, Entity entering, Coords coords, Entity transport) {
        //no stacking violations on the low-atmosphere and space maps
        if(!game.getBoard().onGround()) {
            return null;
        }

        boolean isMech = entering instanceof Mech;
        Entity firstEntity = transport;
        int totalUnits = 1;
        int thisLowStackingLevel = entering.getElevation();
        if ((coords != null) && (entering.getPosition() != null)) {
            thisLowStackingLevel = entering.calcElevation(game.getBoard()
                    .getHex(entering.getPosition()), game.getBoard().getHex(
                    coords));
        }
        int thisHighStackingLevel = thisLowStackingLevel + entering.height();

        // Walk through the entities in the given hex.
        for (Enumeration<Entity> i = game.getEntities(coords); i.hasMoreElements();) {
            final Entity inHex = i.nextElement();

            int lowStackinglevel = inHex.getElevation();
            int highStackingLevel = lowStackinglevel + inHex.height();

            // Only do all this jazz if they're close enough together on level
            // to interfere.
            if ((thisLowStackingLevel <= highStackingLevel) && (thisHighStackingLevel >= lowStackinglevel)) {
                // Don't compare the entering entity to itself.
                if (inHex.equals(entering)) {
                    continue;
                }

                // Ignore the transport of the entering entity.
                if (inHex.equals(transport)) {
                    continue;
                }

                // DFAing units don't count towards stacking
                if (inHex.isMakingDfa()) {
                    continue;
                }

                // If the entering entity is a mech,
                // then any other mech in the hex is a violation.
                // Unless grappled
                if (isMech && (inHex instanceof Mech) && (((Mech) inHex).getGrappled() != entering.getId())) {
                    return inHex;
                }

                totalUnits++;
                // If the new one is the most
                if (totalUnits > 4) {
                    // Arbitrarily return this one, because we can, and it's
                    // simpler.
                    return inHex;
                }

                // Otherwise, if there are two present entities controlled
                // by this player, returns a random one of the two.
                // Somewhat arbitrary, but how else should we resolve it?
                if (!inHex.getOwner().isEnemyOf(entering.getOwner())) {
                    if (firstEntity == null) {
                        firstEntity = inHex;
                    } else {
                        return Compute.d6() > 3 ? firstEntity : inHex;
                    }
                }
            }
        }

        // okay, all clear
        return null;
    }

    /**
     * Returns true if there is any unit that is an enemy of the specified unit
     * in the specified hex. This is only called for stacking purposes, and so
     * does not return true if the enemy unit is currenly making a DFA.
     */
    public static boolean isEnemyIn(IGame game, Entity entity, Coords coords,
            boolean onlyMechs, boolean ignoreInfantry, int enLowEl) {
        int enHighEl = enLowEl + entity.getHeight();
        for (Enumeration<Entity> i = game.getEntities(coords); i.hasMoreElements();) {
            final Entity inHex = i.nextElement();
            int inHexEnLowEl = inHex.getElevation();
            int inHexEnHighEl = inHexEnLowEl + inHex.getHeight();
            if ((!onlyMechs || (inHex instanceof Mech))
                    && !(ignoreInfantry && (inHex instanceof Infantry))
                    && inHex.isEnemyOf(entity) && !inHex.isMakingDfa()
                    && (enLowEl <= inHexEnHighEl) && (enHighEl >= inHexEnLowEl)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return true if a piloting skill roll is needed to traverse the terrain
     */
    public static boolean isPilotingSkillNeeded(IGame game, int entityId,
            Coords src, Coords dest, int movementType, boolean isTurning,
            boolean prevStepIsOnPavement, int srcElevation, int destElevation,
            MovePath path) {
        final Entity entity = game.getEntity(entityId);
        final IHex srcHex = game.getBoard().getHex(src);
        final IHex destHex = game.getBoard().getHex(dest);
        final boolean isInfantry = (entity instanceof Infantry);
        final boolean isPavementStep = Compute.canMoveOnPavement(game, src, dest, path);

        // arguments valid?
        if (entity == null) {
            throw new IllegalArgumentException("Entity invalid.");
        }
        if (src.distance(dest) > 1) {
            throw new IllegalArgumentException("Coordinates must be adjacent.");
        }

        // let's only worry about actual movement, please
        if (src.equals(dest)) {
            return false;
        }

        // check for rubble
        if ((movementType != IEntityMovementType.MOVE_JUMP)
                && (destHex.terrainLevel(Terrains.RUBBLE) > 0)
                && (entity.getMovementMode() != IEntityMovementMode.VTOL)
                && !isInfantry) {
            return true;
        }

        // check for swamp
        if (destHex.containsTerrain(Terrains.SWAMP)
                && !(entity.getElevation() > destHex.getElevation())
                && (entity.getMovementMode() != IEntityMovementMode.HOVER)
                && (entity.getMovementMode() != IEntityMovementMode.VTOL)
                && (movementType != IEntityMovementType.MOVE_JUMP)
                && (entity.getMovementMode() != IEntityMovementMode.WIGE)) {
            return true;
        }

        // check for thin ice
        if (destHex.containsTerrain(Terrains.ICE)
                && destHex.containsTerrain(Terrains.WATER)
                && !(entity.getElevation() > destHex.getElevation())
                && !isPavementStep
                && (movementType != IEntityMovementType.MOVE_JUMP)) {
            return true;
        }

        // Check for water unless we're a hovercraft or naval or using a bridge
        // or flying.
        if ((movementType != IEntityMovementType.MOVE_JUMP)
                && !(entity.getElevation() > destHex.surface())
                && !((entity.getMovementMode() == IEntityMovementMode.HOVER)
                        || (entity.getMovementMode() == IEntityMovementMode.NAVAL)
                        || (entity.getMovementMode() == IEntityMovementMode.HYDROFOIL)
                        || (entity.getMovementMode() == IEntityMovementMode.SUBMARINE)
                        || (entity.getMovementMode() == IEntityMovementMode.INF_UMU)
                        || (entity.getMovementMode() == IEntityMovementMode.BIPED_SWIM)
                        || (entity.getMovementMode() == IEntityMovementMode.QUAD_SWIM) || (entity
                        .getMovementMode() == IEntityMovementMode.WIGE))
                && (destHex.terrainLevel(Terrains.WATER) > 0) && !isPavementStep) {
            return true;
        }

        // Check for skid. Please note, the skid will be rolled on the
        // current step, but starts from the previous step's location.
        // TODO: add check for elevation of pavement, road,
        // or bridge matches entity elevation.
        /*
         * Bug 754610: Revert fix for bug 702735. if ( (
         * srcHex.contains(Terrain.PAVEMENT) || srcHex.contains(Terrain.ROAD) ||
         * srcHex.contains(Terrain.BRIDGE) )
         */
        if (((prevStepIsOnPavement && (movementType == IEntityMovementType.MOVE_RUN))
                || ((srcHex.containsTerrain(Terrains.ICE))
                && (movementType != IEntityMovementType.MOVE_JUMP)))
                && (entity.getMovementMode() != IEntityMovementMode.HOVER)
                && (entity.getMovementMode() != IEntityMovementMode.WIGE)
                && isTurning && !isInfantry) {
            return true;
        }

        // If we entering a building, all non-infantry
        // need to make a piloting check to avoid damage.
        if ((destElevation < destHex.terrainLevel(Terrains.BLDG_ELEV))
                && !(entity instanceof Infantry)) {
            return true;
        }

        // check sideslips
        if ((entity instanceof VTOL)
                || (entity.getMovementMode() == IEntityMovementMode.HOVER)
                || (entity.getMovementMode() == IEntityMovementMode.WIGE)) {
            if (isTurning
                    && ((movementType == IEntityMovementType.MOVE_RUN)
                            || (movementType == IEntityMovementType.MOVE_VTOL_RUN))) {
                return true;
            }
        }

        return false;
    }

    /**
     * Can the defending unit be displaced from the source to the destination?
     */
    public static boolean isValidDisplacement(IGame game, int entityId, Coords src, int direction) {
        return Compute.isValidDisplacement(game, entityId, src, src.translated(direction));
    }

    /**
     * Can the defending unit be displaced from the source to the destination?
     */
    public static boolean isValidDisplacement(IGame game, int entityId, Coords src, Coords dest) {
        final Entity entity = game.getEntity(entityId);
        final IHex srcHex = game.getBoard().getHex(src);
        final IHex destHex = game.getBoard().getHex(dest);
        final ArrayList<Coords> intervening = Coords.intervening(src, dest);
        final int direction = src.direction(dest);

        // arguments valid?
        if (entity == null) {
            throw new IllegalArgumentException("Entity invalid.");
        }

        // an easy check
        if (!game.getBoard().contains(dest)) {
            if (game.getOptions().booleanOption("push_off_board")) {
                return true;
            }
            return false;
        }

        // can't be displaced into prohibited terrain
        // unless we're displacing a tracked or wheeled vee into water
        if (entity.isHexProhibited(destHex)
                && !((entity instanceof Tank)
                        && destHex.containsTerrain(Terrains.WATER)
                        && ((entity.movementMode == IEntityMovementMode.TRACKED)
                                || (entity.movementMode == IEntityMovementMode.WHEELED)))) {
            return false;
        }

        // can't go up more levels than normally possible
        for (Coords c : intervening) {
            // ignore off-board hexes
            if (!game.getBoard().contains(c)) {
                continue;
            }
            final IHex hex = game.getBoard().getHex(c);
            int change = entity.elevationOccupied(hex) - entity.elevationOccupied(srcHex);
            if (change > entity.getMaxElevationChange()) {
                return false;
            }
        }

        // if there's an entity in the way, can they be displaced in that
        // direction?
        Entity inTheWay = Compute.stackingViolation(game, entityId, dest);
        if (inTheWay != null) {
            return Compute.isValidDisplacement(game, inTheWay.getId(), inTheWay.getPosition(), direction);
        }

        // okay, that's about all the checks
        return true;
    }

    /**
     * Gets a valid displacement, from the hexes around src, as close to the
     * original direction as is possible.
     *
     * @return valid displacement coords, or null if none
     */
    public static Coords getValidDisplacement(IGame game, int entityId, Coords src, int direction) {
        // check the surrounding hexes, nearest to the original direction first
        int[] offsets = { 0, 1, 5, 2, 4, 3 };
        for (int offset : offsets) {
            Coords dest = src.translated((direction + offset) % 6);
            if (Compute.isValidDisplacement(game, entityId, src, dest)) {
                return dest;
            }
        }
        // have fun being insta-killed!
        return null;
    }

    /**
     * Gets a preferred displacement. Right now this picks the surrounding hex
     * with the same elevation as original hex, if not available it picks the
     * highest elevation that is a valid displacement.
     * This will preferably not displace into friendly units
     *
     * @return valid displacement coords, or null if none
     */
    public static Coords getPreferredDisplacement(IGame game, int entityId, Coords src, int direction) {
        final Entity entity = game.getEntity(entityId);
        int highestElev = Integer.MIN_VALUE;
        Coords highest = null;

        // check the surrounding hexes, nearest to the original direction first
        int[] offsets = { 0, 1, 5, 2, 4, 3 };
        // first, try not to displace into friendly units
        for (int offset : offsets) {
            Coords dest = src.translated((direction + offset) % 6);
            if (Compute.isValidDisplacement(game, entityId, src, dest)
                    && game.getBoard().contains(dest)) {
                Enumeration<Entity> entities = game.getFriendlyEntities(dest, game.getEntity(entityId));
                if (entities.hasMoreElements()) {
                    // friendly unit here, try next hex
                    continue;
                }
                IHex hex = game.getBoard().getHex(dest);
                int elevation = entity.elevationOccupied(hex);
                if (elevation > highestElev) {
                    highestElev = elevation;
                    highest = dest;
                }
                // preferably, go to same elevation
                if (elevation == entity.getElevation()) {
                    return dest;
                }
            }
        }
        if (highest != null) {
            return highest;
        }
        // ok, all hexes occupied, now displace preferably to same elevation,
        // else highest
        for (int offset : offsets) {
            Coords dest = src.translated((direction + offset) % 6);
            if (Compute.isValidDisplacement(game, entityId, src, dest)
                    && game.getBoard().contains(dest)) {
                IHex hex = game.getBoard().getHex(dest);
                int elevation = entity.elevationOccupied(hex);
                if (elevation > highestElev) {
                    highestElev = elevation;
                    highest = dest;
                }
                // preferably, go to same elevation
                if (elevation == entity.getElevation()) {
                    return dest;
                }
            }
        }
        return highest;
    }

    /**
     * Gets a hex to displace a missed charge to. Picks left or right, first
     * preferring higher hexes, then randomly, or returns the base hex if
     * they're impassible.
     */
    public static Coords getMissedChargeDisplacement(IGame game, int entityId, Coords src, int direction) {
        Coords first = src.translated((direction + 1) % 6);
        Coords second = src.translated((direction + 5) % 6);
        IHex firstHex = game.getBoard().getHex(first);
        IHex secondHex = game.getBoard().getHex(second);
        Entity entity = game.getEntity(entityId);

        if ((firstHex == null) || (secondHex == null)) {
            // leave it, will be handled
        } else if (entity.elevationOccupied(firstHex) > entity.elevationOccupied(secondHex)) {
            // leave it
        } else if (entity.elevationOccupied(firstHex) < entity.elevationOccupied(secondHex)) {
            // switch
            Coords temp = first;
            first = second;
            second = temp;
        } else if (Compute.d6() > 3) {
            // switch randomly
            Coords temp = first;
            first = second;
            second = temp;
        }

        if (Compute.isValidDisplacement(game, entityId, src, src.direction(first)) && game.getBoard().contains(first)) {
            return first;
        } else if (Compute.isValidDisplacement(game, entityId, src, src.direction(second)) && game.getBoard().contains(second)) {
            return second;
        } else {
            return src;
        }
    }

    /**
     * Finds the best spotter for the attacker. The best spotter is the one with
     * the lowest attack modifiers, of course. LOS modifiers and movement are
     * considered.
     */
    public static Entity findSpotter(IGame game, Entity attacker, Targetable target) {
        Entity spotter = null;
        int taggedBy = -1;
        if (target instanceof Entity) {
            taggedBy = ((Entity) target).getTaggedBy();
        }
        ToHitData bestMods = new ToHitData(TargetRoll.IMPOSSIBLE, "");

        for (java.util.Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity other = i.nextElement();
            if (((other.isSpotting() && (other.getSpotTargetId() == target.getTargetId()))
                    || (taggedBy == other.getId())) && !attacker.isEnemyOf(other)) {
                // what are this guy's mods to the attack?
                LosEffects los = LosEffects.calculateLos(game, other.getId(), target);
                ToHitData mods = los.losModifiers(game);
                los.setTargetCover(LosEffects.COVER_NONE);
                mods.append(Compute.getAttackerMovementModifier(game, other.getId()));
                if (other.isAttackingThisTurn()) {
                    mods.addModifier(1, "spotter is making an attack this turn");
                }
                // is this guy a better spotter?
                if ((spotter == null) || (mods.getValue() < bestMods.getValue())) {
                    spotter = other;
                    bestMods = mods;
                }
            }
        }

        return spotter;
    }

    public static ToHitData getImmobileMod(Targetable target) {
        return Compute.getImmobileMod(target, Entity.LOC_NONE, IAimingModes.AIM_MODE_NONE);
    }

    public static ToHitData getImmobileMod(Targetable target, int aimingAt, int aimingMode) {
        if (target.isImmobile()) {
            if ((target instanceof Mech) && (aimingAt == Mech.LOC_HEAD) && (aimingMode == IAimingModes.AIM_MODE_IMMOBILE)) {
                return new ToHitData(3, "aiming at head");
            }
            return new ToHitData(-4, "target immobile");
        }
        return null;
    }

    /**
     * Determines the to-hit modifier due to range for an attack with the
     * specified parameters. Includes minimum range, infantry 0-range mods, and
     * target stealth mods. Accounts for friendly C3 units.
     *
     * @return the modifiers
     */
    public static ToHitData getRangeMods(IGame game, Entity ae, int weaponId, Targetable target) {
        Mounted weapon = ae.getEquipment(weaponId);
        WeaponType wtype = (WeaponType) weapon.getType();
        int[] weaponRanges = wtype.getRanges(weapon);
        boolean isAttackerInfantry = (ae instanceof Infantry);
        boolean isWeaponInfantry = wtype.hasFlag(WeaponType.F_INFANTRY);
        boolean isIndirect = ((wtype.getAmmoType() == AmmoType.T_LRM) || (wtype.getAmmoType() == AmmoType.T_MML) || (wtype.getAmmoType() == AmmoType.T_EXLRM) || (wtype.getAmmoType() == AmmoType.T_TBOLT_5) || (wtype.getAmmoType() == AmmoType.T_TBOLT_10) || (wtype.getAmmoType() == AmmoType.T_TBOLT_15) || (wtype.getAmmoType() == AmmoType.T_TBOLT_20) || (wtype.getAmmoType() == AmmoType.T_LRM_TORPEDO)) && weapon.curMode().equals("Indirect");
        boolean useExtremeRange = game.getOptions().booleanOption("tacops_range");

        if(ae instanceof Aero) {
            useExtremeRange = true;
        }

        ToHitData mods = new ToHitData();

        //
        // modifiy the ranges for PPCs when field inhibitors are turned off
        // TODO: See above, it should be coded elsewhere...
        //
        if (wtype.hasFlag(WeaponType.F_PPC)) {
            if (game.getOptions().booleanOption("tacops_ppc_inhibitors")) {
                if ((weapon.curMode() != null) && weapon.curMode().equals("Field Inhibitor OFF")) {
                    weaponRanges[RangeType.RANGE_MINIMUM] = 0;
                }
            }
        }

        // Hotloaded weapons
        if (weapon.isHotLoaded() && game.getOptions().booleanOption("tacops_hotload")) {
            weaponRanges[RangeType.RANGE_MINIMUM] = 0;
        }

        // is water involved?
        IHex targHex = game.getBoard().getHex(target.getPosition());
        int targTop = target.absHeight();
        int targBottom = target.getElevation();

        boolean targetInPartialWater = false;
        boolean targetUnderwater = false;
        boolean weaponUnderwater = (ae.getLocationStatus(weapon.getLocation()) == ILocationExposureStatus.WET);
        if ((target.getTargetType() == Targetable.TYPE_ENTITY) && targHex.containsTerrain(Terrains.WATER) && (targBottom < 0)) {
            if (targTop >= 0) {
                targetInPartialWater = true;
            } else {
                targetUnderwater = true;
            }
        }

        // allow naval units on surface to be attacked from above or below
        Entity te = null;
        if (target instanceof Entity) {
            te = (Entity) target;
            if ((targBottom == 0) && (UnitType.determineUnitTypeCode(te) == UnitType.NAVAL)) {
                targetInPartialWater = true;
            }
        }
        // allow naval units to target underwater units,
        // torpedo tubes are mounted underwater
        if ((targetUnderwater || (wtype.getAmmoType() == AmmoType.T_LRM_TORPEDO)
                || (wtype.getAmmoType() == AmmoType.T_SRM_TORPEDO))
                && (UnitType.determineUnitTypeCode(ae) == UnitType.NAVAL)) {
            weaponUnderwater = true;
            weaponRanges = wtype.getWRanges();
        }
        // allow ice to be cleared from below
        if (targHex.containsTerrain(Terrains.WATER) && (target.getTargetType() == Targetable.TYPE_HEX_CLEAR)) {
            targetInPartialWater = true;
        }

        if (weaponUnderwater) {
            weaponRanges = wtype.getWRanges();
            boolean MPM = false;
            if ((wtype.getAmmoType() == AmmoType.T_SRM)
                    || (wtype.getAmmoType() == AmmoType.T_MRM)
                    || (wtype.getAmmoType() == AmmoType.T_LRM)
                    || (wtype.getAmmoType() == AmmoType.T_MML)) {
                AmmoType atype = (AmmoType) weapon.getLinked().getType();
                if (atype.getMunitionType() == AmmoType.M_TORPEDO) {
                    weaponRanges = wtype.getRanges(weapon);
                } else if (atype.getMunitionType() == AmmoType.M_MULTI_PURPOSE) {
                    weaponRanges = wtype.getRanges(weapon);
                    MPM = true;
                }
            }

            // HACK on ranges: for those without underwater range,
            // long == medium; iteration in rangeBracket() allows this
            if (weaponRanges[RangeType.RANGE_SHORT] == 0) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Weapon cannot fire underwater.");
            }
            if (!targetUnderwater && !targetInPartialWater && !MPM) {
                // target on land or over water
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Weapon underwater, but not target.");
            }
            // special case: mechs can only fire upper body weapons at surface
            // naval
            if ((te != null) && (UnitType.determineUnitTypeCode(te) == UnitType.NAVAL)
                    && (ae instanceof Mech) && (ae.height() > 0)
                    && (ae.getElevation() == -1)) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Partially submerged mech cannot fire leg weapons at surface naval vessels.");
            }
        } else if (targetUnderwater) {
            return new ToHitData(TargetRoll.IMPOSSIBLE, "Target underwater, but not weapon.");
        } else if ((wtype.getAmmoType() == AmmoType.T_LRM_TORPEDO) || (wtype.getAmmoType() == AmmoType.T_SRM_TORPEDO)) {
            // Torpedos only fire underwater.
            return new ToHitData(TargetRoll.IMPOSSIBLE, "Weapon can only fire underwater.");
        }

        //if Aero then adjust to stanard ranges
        if(ae instanceof Aero) {
            weaponRanges = wtype.getATRanges();
        }

        // determine base distance & range bracket
        int distance = Compute.effectiveDistance(game, ae, target);
        int range = RangeType.rangeBracket(distance, weaponRanges, useExtremeRange);

        int maxRange = wtype.getMaxRange();
        //if this is a weapon bay I need to cycle through weapons
        if(wtype instanceof BayWeapon) {
            for(int wId : weapon.getBayWeapons()) {
                Mounted bayW = ae.getEquipment(wId);
                WeaponType bayWType = (WeaponType)bayW.getType();
                if(bayWType.getMaxRange() > maxRange) {
                    maxRange = bayWType.getMaxRange();
                }
            }
        }

        //if aero and greater than max range then swith to range_out
        if((ae instanceof Aero) && (range > maxRange)) {
            range = RangeType.RANGE_OUT;
        }

        // short circuit if at zero range or out of range
        if (range == RangeType.RANGE_OUT) {
            return new ToHitData(TargetRoll.AUTOMATIC_FAIL, "Target out of range");
        }
        if ((distance == 0) && !isAttackerInfantry && !(ae instanceof Aero)
                && !((ae instanceof Mech) && (((Mech) ae).getGrappled() == target.getTargetId()))) {
            return new ToHitData(TargetRoll.AUTOMATIC_FAIL, "Only infantry shoot at zero range");
        }

        //Account for "dead zones" between Aeros at different altitudes
        if(game.getBoard().inAtmosphere() && (ae instanceof Aero) && (target instanceof Aero)) {
            int altDiff = Math.abs(ae.getElevation() - target.getElevation());
            int realDistance = distance - altDiff;
            if(altDiff >= realDistance) {
                return new ToHitData(TargetRoll.IMPOSSIBLE,
                "Target in dead zone");
            }
        }

        // find any c3 spotters that could help
        Entity c3spotter = Compute.findC3Spotter(game, ae, target);
        if (isIndirect) {
            c3spotter = ae; // no c3 when using indirect fire
        }
        if (isIndirect && game.getOptions().booleanOption("indirect_fire")
                && !game.getOptions().booleanOption("indirect_always_possible")
                && LosEffects.calculateLos(game, ae.getId(), target).canSee()) {
            return new ToHitData(TargetRoll.IMPOSSIBLE, "Indirect fire impossible with direct LOS");
        }

        int c3dist = Compute.effectiveDistance(game, c3spotter, target);
        int c3range = RangeType.rangeBracket(c3dist, weaponRanges, useExtremeRange);

        /*
         * Tac Ops Extreme Range Rule p. 85 if the weapons normal range is Extreme then C3
         * uses the next highest range bracket, i.e. medium instead of short.
         */
        if ( range == RangeType.RANGE_EXTREME ) {
            c3range++;
        }

        // determine which range we're using
        int usingRange = Math.min(range, c3range);

        String targSysType = "";
        // Get the targeting system type string ready, if necessary
        if ((ae.getTargSysType() == MiscType.T_TARGSYS_LONGRANGE) && (ae.getTargSysType() == MiscType.T_TARGSYS_SHORTRANGE)) {
            targSysType = " (w/" + MiscType.getTargetSysName(ae.getTargSysType()) + ")";
        }

        // add range modifier
        if (usingRange == range) {
            // no c3 adjustment
            if (((range == RangeType.RANGE_SHORT) || (range == RangeType.RANGE_MINIMUM)) && (ae.getShortRangeModifier() != 0)) {
                mods.addModifier(ae.getShortRangeModifier(), "short range" + targSysType);
            } else if (range == RangeType.RANGE_MEDIUM) {
                // Right now, the range-mod affecting targeting systems DON'T
                // affect medium range, so we won't add that here ever.
                mods.addModifier(ae.getMediumRangeModifier(), "medium range");
            } else if (range == RangeType.RANGE_LONG) {
                // Protos that loose head sensors can't shoot long range.
                if ((ae instanceof Protomech) && (2 == ((Protomech) ae).getCritsHit(Protomech.LOC_HEAD))) {
                    mods.addModifier(TargetRoll.IMPOSSIBLE, "No long range attacks with destroyed head sensors.");
                } else {
                    mods.addModifier(ae.getLongRangeModifier(), "long range" + targSysType);
                }
            } else if (range == RangeType.RANGE_EXTREME) {
                // Protos that loose head sensors can't shoot extreme range.
                if ((ae instanceof Protomech) && (2 == ((Protomech) ae).getCritsHit(Protomech.LOC_HEAD))) {
                    mods.addModifier(TargetRoll.IMPOSSIBLE, "No extreme range attacks with destroyed head sensors.");
                } else {
                    mods.addModifier(ae.getExtremeRangeModifier(), "extreme range" + targSysType);
                }
            }
        } else {
            // report c3 adjustment
            if ((c3range == RangeType.RANGE_SHORT) || (c3range == RangeType.RANGE_MINIMUM)) {
                mods.addModifier(ae.getShortRangeModifier(), "short range due to C3 spotter" + targSysType);
            } else if (c3range == RangeType.RANGE_MEDIUM) {
                mods.addModifier(ae.getMediumRangeModifier(), "medium range due to C3 spotter" + targSysType);
            } else if (c3range == RangeType.RANGE_LONG) {
                mods.addModifier(ae.getLongRangeModifier(), "long range due to C3 spotter" + targSysType);
            }
        }

        // add infantry zero-range modifier
        // TODO: this is not the right place to hardcode these
        if (isWeaponInfantry && (distance == 0)) {
            // Infantry platoons attacking with infantry weapons can attack
            // in the same hex with a base of 2, except for flamers and
            // SRMs/LRMs, which have a base of 3.
            if (wtype.hasFlag(WeaponType.F_FLAMER)) {
                mods.addModifier(-1, "infantry flamer assault");
            } else if ((wtype.getAmmoType() == AmmoType.T_SRM) || (wtype.getAmmoType() == AmmoType.T_LRM)) {
                mods.addModifier(-1, "infantry missile assault");
            } else {
                mods.addModifier(-2, "infantry assault");
            }
        }

        // add minimum range modifier
        int minRange = weaponRanges[RangeType.RANGE_MINIMUM];
        if ((minRange > 0) && (distance <= minRange)) {
            int minPenalty = (minRange - distance) + 1;
            mods.addModifier(minPenalty, "minimum range");
        }

        // add any target stealth modifier
        if (target instanceof Entity) {
            TargetRoll tmpTR = ((Entity) target).getStealthModifier(usingRange, ae);
            if (tmpTR.getValue() != 0) {
                mods.append(((Entity) target).getStealthModifier(usingRange, ae));
            }
        }

        return mods;
    }

    /**
     * Finds the effective distance between an attacker and a target. Includes
     * the distance bonus if the attacker and target are in the same building
     * and on different levels.
     *
     * @return the effective distance
     */
    public static int effectiveDistance(IGame game, Entity attacker, Targetable target) {
        int distance = attacker.getPosition().distance(target.getPosition());

        // If the attack is completely inside a building, add the difference
        // in elevations between the attacker and target to the range.
        // TODO: should the player be explcitly notified?
        //also for Aeros in atmosphere
        if (Compute.isInSameBuilding(game, attacker, target)  ||
                ((attacker instanceof Aero) && (target instanceof Aero) && game.getBoard().inAtmosphere())) {
            int aElev = attacker.getElevation();
            int tElev = target.getElevation();
            distance += Math.abs(aElev - tElev);
        }

        return distance;
    }

    /**
     * Attempts to find a C3 spotter that is closer to the target than the
     * attacker.
     *
     * @return A closer C3 spotter, or the attack if no spotters are found
     */
    private static Entity findC3Spotter(IGame game, Entity attacker, Targetable target) {
        //TODO: underwater units can't spot for overwater units and vice versa
        if (!attacker.hasC3() && !attacker.hasC3i()) {
            return attacker;
        }

        if (attacker.hasC3i()) {
            return Compute.findC3iSpotter(game, attacker, target);
        }

        Entity c3spotter = attacker;
        int c3range = attacker.getPosition().distance(target.getPosition());

        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity friend = i.nextElement();

            // TODO : can units being transported be used for C3 spotting?
            if (attacker.equals(friend) || !friend.isActive() || !attacker.onSameC3NetworkAs(friend) || !friend.isDeployed()) {
                continue; // useless to us...
            }

            int buddyRange = Compute.effectiveDistance(game, friend, target);
            if (buddyRange < c3range) {
                c3range = buddyRange;
                c3spotter = friend;
            }
        }
        return c3spotter;
    }

    /**
     * find a c3i spotter that is closer to the target than the
     * attacker.
     * @param game
     * @param attacker
     * @param target
     * @return
     */
    private static Entity findC3iSpotter(IGame game, Entity attacker, Targetable target) {
        if (!attacker.hasC3() && !attacker.hasC3i()) {
            return attacker;
        }
        Entity c3spotter = attacker;

        ArrayList<Entity> network = new ArrayList<Entity>();

        for (Enumeration<Entity> i = game.getEntities(); i.hasMoreElements();) {
            Entity friend = i.nextElement();

            if (attacker.equals(friend) || !attacker.onSameC3NetworkAs(friend, true) || !friend.isDeployed()) {
                continue; // useless to us...
            }

            int buddyRange = Compute.effectiveDistance(game, friend, target);

            boolean added = false;
            //but everyone in the C3i network into a list and sort it by range.
            for (int pos = 0; pos < network.size(); pos++) {
                if (Compute.effectiveDistance(game, network.get(pos), target) >= buddyRange) {
                    network.add(pos, friend);
                    added = true;
                    break;
                }
            }

            if ( !added ) {
                network.add(friend);
            }
        }

        int position = 0;
        for (Entity spotter : network) {

            for (int count = position++; count < network.size(); count++) {
                if (Compute.canCompleteNodePath(spotter, attacker, network, count)) {
                    return spotter;
                }
            }
        }
        return c3spotter;
    }

    private static boolean canCompleteNodePath(Entity start, Entity end, ArrayList<Entity>network, int startPosition) {

        Entity spotter = network.get(startPosition);

        //Last position cannot get to this one. go to the next person
        if ( Compute.isAffectedByECM(spotter, start.getPosition(), spotter.getPosition()) ) {
            return false;
        }

        if ( !Compute.isAffectedByECM(spotter, spotter.getPosition(), end.getPosition()) ) {
            return true;
        }

        for (++startPosition ; startPosition < network.size(); startPosition++) {
            if ( Compute.canCompleteNodePath(spotter,end, network, startPosition) ) {
                return true;
            }
        }

        return false;
    }

    /**
     * Gets the modifiers, if any, that the mech receives from being prone.
     *
     * @return any applicable modifiers due to being prone
     */
    public static ToHitData getProneMods(IGame game, Entity attacker, int weaponId) {
        if (!attacker.isProne()) {
            return null; // no modifier
        }
        ToHitData mods = new ToHitData();
        Mounted weapon = attacker.getEquipment(weaponId);
        if (attacker.entityIsQuad()) {
            int legsDead = ((Mech) attacker).countBadLegs();
            if (legsDead == 0) {
                // No legs destroyed: no penalty and can fire all weapons
                return null; // no modifier
            } else if (legsDead >= 3) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone with three or more legs destroyed.");
            }
            // we have one or two dead legs...

            // Need an intact front leg
            if (attacker.isLocationBad(Mech.LOC_RARM) && attacker.isLocationBad(Mech.LOC_LARM)) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone with both front legs destroyed.");
            }

            // front leg-mounted weapons have addidional trouble
            if ((weapon.getLocation() == Mech.LOC_RARM) || (weapon.getLocation() == Mech.LOC_LARM)) {
                int otherArm = weapon.getLocation() == Mech.LOC_RARM ? Mech.LOC_LARM : Mech.LOC_RARM;
                // check previous attacks for weapons fire from the other arm
                if (Compute.isFiringFromArmAlready(game, weaponId, attacker, otherArm)) {
                    return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone and firing from other front leg already.");
                }
            }
            // can't fire rear leg weapons
            if ((weapon.getLocation() == Mech.LOC_LLEG) || (weapon.getLocation() == Mech.LOC_RLEG)) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Can't fire rear leg-mounted weapons while prone with destroyed legs.");
            }
            mods.addModifier(2, "attacker prone");
        } else {
            int l3ProneFiringArm = Entity.LOC_NONE;

            if (attacker.isLocationBad(Mech.LOC_RARM) || attacker.isLocationBad(Mech.LOC_LARM)) {
                if (game.getOptions().booleanOption("tacops_prone_fire")) {
                    // Can fire with only one arm
                    if (attacker.isLocationBad(Mech.LOC_RARM) && attacker.isLocationBad(Mech.LOC_LARM)) {
                        return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone with both arms destroyed.");
                    }

                    l3ProneFiringArm = attacker.isLocationBad(Mech.LOC_RARM) ? Mech.LOC_LARM : Mech.LOC_RARM;
                } else {
                    // must have an arm intact
                    return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone with one or both arms destroyed.");
                }
            }

            // arm-mounted weapons have addidional trouble
            if ((weapon.getLocation() == Mech.LOC_RARM) || (weapon.getLocation() == Mech.LOC_LARM)) {
                if (l3ProneFiringArm == weapon.getLocation()) {
                    return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone and propping up with this arm.");
                }

                int otherArm = weapon.getLocation() == Mech.LOC_RARM ? Mech.LOC_LARM : Mech.LOC_RARM;
                // check previous attacks for weapons fire from the other arm
                if (Compute.isFiringFromArmAlready(game, weaponId, attacker, otherArm)) {
                    return new ToHitData(TargetRoll.IMPOSSIBLE, "Prone and firing from other arm already.");
                }
            }
            // can't fire leg weapons
            if ((weapon.getLocation() == Mech.LOC_LLEG) || (weapon.getLocation() == Mech.LOC_RLEG)) {
                return new ToHitData(TargetRoll.IMPOSSIBLE, "Can't fire leg-mounted weapons while prone.");
            }
            mods.addModifier(2, "attacker prone");

            if (l3ProneFiringArm != Entity.LOC_NONE) {
                mods.addModifier(1, "attacker propping on single arm");
            }
        }
        return mods;
    }

    /**
     * Checks to see if there is an attack previous to the one with this weapon
     * from the specified arm.
     *
     * @return true if there is a previous attack from this arm
     */
    private static boolean isFiringFromArmAlready(IGame game, int weaponId, final Entity attacker, int armLoc) {
        int torsoLoc = Mech.getInnerLocation(armLoc);
        for (Enumeration<EntityAction> i = game.getActions(); i.hasMoreElements();) {
            EntityAction ea = i.nextElement();
            if (!(ea instanceof WeaponAttackAction)) {
                continue;
            }
            WeaponAttackAction prevAttack = (WeaponAttackAction) ea;
            // stop when we get to this weaponattack (does this always work?)
            if ((prevAttack.getEntityId() == attacker.getId()) && (prevAttack.getWeaponId() == weaponId)) {
                break;
            }
            if (((prevAttack.getEntityId() == attacker.getId()) && (attacker.getEquipment(prevAttack.getWeaponId()).getLocation() == armLoc)) || ((prevAttack.getEntityId() == attacker.getId()) && (attacker.getEquipment(prevAttack.getWeaponId()).getLocation() == torsoLoc) && attacker.getEquipment(prevAttack.getWeaponId()).isSplit())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Adds any damage modifiers from arm critical hits or sensor damage.
     *
     * @return Any applicable damage modifiers
     */
    public static ToHitData getDamageWeaponMods(Entity attacker, Mounted weapon) {
        ToHitData mods = new ToHitData();
        if (attacker instanceof Protomech) {
            // Head criticals add to target number of all weapons.
            int hits = ((Protomech) attacker).getCritsHit(Protomech.LOC_HEAD);
            if (hits > 0) {
                mods.addModifier(hits, hits + " head critical(s)");
            }

            // Arm mounted (and main gun) weapons get DRMs from arm crits.
            switch (weapon.getLocation()) {
            case Protomech.LOC_LARM:
            case Protomech.LOC_RARM:
                hits = ((Protomech) attacker).getCritsHit(weapon.getLocation());
                if (hits > 0) {
                    mods.addModifier(hits, hits + " arm critical(s)");
                }
                break;
            case Protomech.LOC_MAINGUN:
                // Main gun is affected by crits in *both* arms.
                hits = ((Protomech) attacker).getCritsHit(Protomech.LOC_LARM);
                hits += ((Protomech) attacker).getCritsHit(Protomech.LOC_RARM);
                if (4 == hits) {
                    mods.addModifier(TargetRoll.IMPOSSIBLE, "Cannot fire main gun with no arms.");
                } else if (hits > 0) {
                    mods.addModifier(hits, hits + " arm critical(s)");
                }
                break;
            }

        } // End attacker-is-Protomech

        // Is the shoulder destroyed?
        else {
            // split weapons need to account for arm actuator hits, too
            // see bug 1363690
            // we don't need to specifically check for weapons split between
            // torso and leg, because for those, the location stored in the
            // Mounted is the leg.
            int location = weapon.getLocation();
            if (weapon.isSplit()) {
                switch (location) {
                case Mech.LOC_LT:
                    location = Mech.LOC_LARM;
                    break;
                case Mech.LOC_RT:
                    location = Mech.LOC_RARM;
                    break;
                default:
                }
            }
            if (attacker.getBadCriticals(CriticalSlot.TYPE_SYSTEM, Mech.ACTUATOR_SHOULDER, location) > 0) {
                mods.addModifier(4, "shoulder actuator destroyed");
            } else {
                // no shoulder hits, add other arm hits
                int actuatorHits = 0;
                if (attacker.getBadCriticals(CriticalSlot.TYPE_SYSTEM, Mech.ACTUATOR_UPPER_ARM, location) > 0) {
                    actuatorHits++;
                }
                if (attacker.getBadCriticals(CriticalSlot.TYPE_SYSTEM, Mech.ACTUATOR_LOWER_ARM, location) > 0) {
                    actuatorHits++;
                }
                if (actuatorHits > 0) {
                    mods.addModifier(actuatorHits, actuatorHits + " destroyed arm actuators");
                }
            }
        }

        // sensors critical hit to attacker
        int sensorHits = attacker.getBadCriticals(CriticalSlot.TYPE_SYSTEM, Mech.SYSTEM_SENSORS, Mech.LOC_HEAD);
        if ((attacker instanceof Mech) && (((Mech) attacker).getCockpitType() == Mech.COCKPIT_TORSO_MOUNTED)) {
            sensorHits += attacker.getBadCriticals(CriticalSlot.TYPE_SYSTEM, Mech.SYSTEM_SENSORS, Mech.LOC_CT);
            if (sensorHits > 1) {
                mods.addModifier(4, "attacker sensors badly damaged");
            } else if (sensorHits > 0) {
                mods.addModifier(2, "attacker sensors damaged");
            }
        } else if (sensorHits > 0) {
            mods.addModifier(2, "attacker sensors damaged");
        }

        return mods;
    }

    /**
     * Determines if the current target is a secondary target, and if so,
     * returns the appropriate modifier.
     *
     * @return The secondary target modifier.
     * @author Ben
     */
    public static ToHitData getSecondaryTargetMod(IGame game, Entity attacker, Targetable target) {
        return Compute.getSecondaryTargetMod(game, attacker, target, false);
    }

    public static ToHitData getSecondaryTargetMod(IGame game, Entity attacker, Targetable target, boolean isSwarm) {

        //large craft do not get secondary target mod
        //http://www.classicbattletech.com/forums/index.php/topic,37661.0.html
        if((attacker instanceof Dropship) || (attacker instanceof Jumpship)) {
            return null;
        }

        boolean curInFrontArc = Compute.isInArc(attacker.getPosition(), attacker.getSecondaryFacing(), target.getPosition(), ARC_FORWARD);

        int primaryTarget = Entity.NONE;
        for (Enumeration<EntityAction> i = game.getActions(); i.hasMoreElements();) {
            Object o = i.nextElement();
            if (!(o instanceof WeaponAttackAction)) {
                continue;
            }
            WeaponAttackAction prevAttack = (WeaponAttackAction) o;
            if (prevAttack.getEntityId() == attacker.getId()) {
                // first front arc target is our primary.
                // if first target is non-front, and either a later target or
                // the current one is in front, use that instead.
                Targetable pte = game.getTarget(prevAttack.getTargetType(), prevAttack.getTargetId());
                // in double blind play, we might not have the target in our
                // local copy of the game. In that case, the sprite won't
                // have the correct to-hit number, but at least we don't crash
                if (pte == null) {
                    continue;
                }
                // When targeting a stealthed Mech, you can _only_ target it,
                // not anything else (BMRr, pg. 147)
                if ((pte instanceof Mech) && ((Entity) pte).isStealthActive() && (pte != target) && !isSwarm) {
                    return new ToHitData(TargetRoll.IMPOSSIBLE, "When targeting a stealthed Mech, can not attack secondary targets");
                }
                if (Compute.isInArc(attacker.getPosition(), attacker.getSecondaryFacing(), pte.getPosition(), ARC_FORWARD)) {
                    primaryTarget = prevAttack.getTargetId();
                    break;
                } else if ((primaryTarget == Entity.NONE) && !curInFrontArc) {
                    primaryTarget = prevAttack.getTargetId();
                }
            }
        }

        if ((primaryTarget == Entity.NONE) || (primaryTarget == target.getTargetId())) {
            // current target is primary target
            return null; // no modifier
        }

        // current target is secondary

        // Infantry can't attack secondary targets, but BA can (TW, page 109).
        if ((attacker instanceof Infantry) && !(attacker instanceof BattleArmor)) {
            return new ToHitData(TargetRoll.IMPOSSIBLE, "Can't have multiple targets.");
        }

        // Stealthed Mechs can't be secondary targets (BMRr, pg. 147)
        if ((target instanceof Mech) && ((Entity) target).isStealthActive()) {
            return new ToHitData(TargetRoll.IMPOSSIBLE, "Can't target Mech with active stealth armor as secondary target");
        }

        if (curInFrontArc || (attacker instanceof BattleArmor)) {
            return new ToHitData(1, "secondary target modifier");
        }
        return new ToHitData(2, "secondary target modifier");
    }

    /**
     * Damage that a mech does with a accidental fall from above.
     */

    public static int getAffaDamageFor(Entity entity) {
        return (int) entity.getWeight() / 10;
    }

    /**
     * Modifier to attacks due to attacker movement
     */
    public static ToHitData getAttackerMovementModifier(IGame game, int entityId) {
        return Compute.getAttackerMovementModifier(game, entityId, game.getEntity(entityId).moved);
    }

    /**
     * Modifier to attacks due to attacker movement
     */
    public static ToHitData getAttackerMovementModifier(IGame game, int entityId, int movement) {
        final Entity entity = game.getEntity(entityId);
        ToHitData toHit = new ToHitData();

        // infantry aren't affected by their own movement.
        if (entity instanceof Infantry) {
            return toHit;
        }

        if ((entity.getMovementMode() == IEntityMovementMode.BIPED_SWIM) || (entity.getMovementMode() == IEntityMovementMode.QUAD_SWIM)) {
            return toHit;
        }

        if ((movement == IEntityMovementType.MOVE_WALK) || (movement == IEntityMovementType.MOVE_VTOL_WALK) || (movement == IEntityMovementType.MOVE_CAREFUL_STAND)) {
            toHit.addModifier(1, "attacker walked");
        } else if ((movement == IEntityMovementType.MOVE_RUN) || (movement == IEntityMovementType.MOVE_VTOL_RUN)) {
            toHit.addModifier(2, "attacker ran");
        } else if (movement == IEntityMovementType.MOVE_SKID) {
            toHit.addModifier(3, "attacker ran and skidded");
        } else if (movement == IEntityMovementType.MOVE_JUMP) {
            toHit.addModifier(3, "attacker jumped");
        } else if (movement == IEntityMovementType.MOVE_OVER_THRUST) {
            toHit.addModifier(2, "over thrust used");
        }

        return toHit;
    }

    /**
     * Modifier to attacks due to spotter movement
     */
    public static ToHitData getSpotterMovementModifier(IGame game, int entityId) {
        return Compute.getSpotterMovementModifier(game, entityId, game.getEntity(entityId).moved);
    }

    /**
     * Modifier to attacks due to spotter movement
     */
    public static ToHitData getSpotterMovementModifier(IGame game, int entityId, int movement) {
        ToHitData toHit = new ToHitData();

        Entity e = game.getEntity(entityId);
        if ((e != null) && (e instanceof Infantry)) {
            return toHit;
        }

        if ((movement == IEntityMovementType.MOVE_WALK) || (movement == IEntityMovementType.MOVE_VTOL_WALK)) {
            toHit.addModifier(1, "spotter walked");
        } else if ((movement == IEntityMovementType.MOVE_RUN) || (movement == IEntityMovementType.MOVE_VTOL_RUN) || (movement == IEntityMovementType.MOVE_SKID)) {
            toHit.addModifier(2, "spotter ran");
        } else if (movement == IEntityMovementType.MOVE_JUMP) {
            toHit.addModifier(3, "spotter jumped");
        }

        return toHit;
    }

    /**
     * Modifier to physical attack BTH due to pilot advantages
     */
    public static void modifyPhysicalBTHForAdvantages(Entity attacker, Entity target, ToHitData toHit, IGame game) {

        if (attacker.getCrew().getOptions().booleanOption("melee_specialist") && (attacker instanceof Mech) && (Compute.getAttackerMovementModifier(game, attacker.getId()).getValue() > 0)) {
            toHit.addModifier(-1, "melee specialist");
        }

        if (attacker.getCrew().getOptions().booleanOption("clan_pilot_training")) {
            toHit.addModifier(1, "clan pilot training");
        }

        // Mek targets that are dodging are harder to hit.

        if ((target != null) && (target instanceof Mech) && target.getCrew().getOptions().booleanOption("dodge_maneuver") && (target.dodging )) {
            toHit.addModifier(2, "target is dodging");
        }
    }

    /**
     * Modifier to attacks due to target movement
     */
    public static ToHitData getTargetMovementModifier(IGame game, int entityId) {
        Entity entity = game.getEntity(entityId);

        if(entity instanceof Aero) {
            return new ToHitData();
        }

    if (game.getOptions().booleanOption("tacops_standing_still") &&
            (entity.moved==IEntityMovementType.MOVE_NONE) &&
            !entity.isImmobile() &&
            !((entity instanceof Infantry) || (entity instanceof VTOL) ||
              (entity instanceof GunEmplacement))) {
            ToHitData toHit = new ToHitData();
            toHit.addModifier(-1, "target didn't move");
            return toHit;
        }

        ToHitData toHit = Compute.getTargetMovementModifier(entity.delta_distance, ((entity.moved == IEntityMovementType.MOVE_JUMP) || (entity.moved == IEntityMovementType.MOVE_VTOL_RUN) || (entity.moved == IEntityMovementType.MOVE_VTOL_WALK)), (entity.moved == IEntityMovementType.MOVE_VTOL_RUN) || (entity.moved == IEntityMovementType.MOVE_VTOL_WALK) || (entity.getMovementMode() == IEntityMovementMode.VTOL));

        // Did the target skid this turn?
        if (entity.moved == IEntityMovementType.MOVE_SKID) {
            toHit.addModifier(2, "target skidded");
        }
        if ((entity.getElevation() > 0) && (entity.getMovementMode() == IEntityMovementMode.WIGE)) {
            toHit.addModifier(1, "target is a flying WiGE");
        }

        return toHit;
    }

    /**
     * Target movement modifer for the specified delta_distance
     */

    public static ToHitData getTargetMovementModifier(int distance, boolean jumped, boolean isVTOL) {
        ToHitData toHit = new ToHitData();
        if (distance == 0) {
            return toHit;
        }

        if ((distance >= 3) && (distance <= 4)) {
            toHit.addModifier(1, "target moved 3-4 hexes");
        } else if ((distance >= 5) && (distance <= 6)) {
            toHit.addModifier(2, "target moved 5-6 hexes");
        } else if ((distance >= 7) && (distance <= 9)) {
            toHit.addModifier(3, "target moved 7-9 hexes");
        } else if ((distance >= 10) && (distance <= 17)) {
            toHit.addModifier(4, "target moved 10-17 hexes");
        } else if ((distance >= 18) && (distance <= 24)) {
            toHit.addModifier(5, "target moved 18-24 hexes");
        } else if (distance >= 25) {
            toHit.addModifier(6, "target moved 25+ hexes");
        }

        if (jumped) {
            if (isVTOL) {
                toHit.addModifier(1, "target VTOL used MPs");
            } else {
                toHit.addModifier(1, "target jumped");
            }
        }

        return toHit;
    }

    /**
     * Modifier to attacks due to attacker terrain
     */
    public static ToHitData getAttackerTerrainModifier(IGame game, int entityId) {
        final Entity attacker = game.getEntity(entityId);
        final IHex hex = game.getBoard().getHex(attacker.getPosition());

        ToHitData toHit = new ToHitData();

        //space screens; bonus depends on number (level)
        if(hex.terrainLevel(Terrains.SCREEN) > 0) {
            toHit.addModifier(hex.terrainLevel(Terrains.SCREEN) + 1, "attacker in screen(s)");
        }

        return toHit;
    }

    /**
     * Modifier to attacks due to target terrain TODO:um....should VTOLs get
     * modifiers for smoke, etc.
     */
    public static ToHitData getTargetTerrainModifier(IGame game, Targetable t) {
        return Compute.getTargetTerrainModifier(game, t, 0);
    }

    public static ToHitData getTargetTerrainModifier(IGame game, Targetable t, int eistatus) {
        return Compute.getTargetTerrainModifier(game, t, eistatus, false);
    }

    public static ToHitData getTargetTerrainModifier(IGame game, Targetable t, int eistatus, boolean attackerInSameBuilding) {
        Entity entityTarget = null;
        IHex hex = game.getBoard().getHex(t.getPosition());
        if (t.getTargetType() == Targetable.TYPE_ENTITY) {
            entityTarget = (Entity) t;
            if (hex == null) {
                entityTarget.setPosition(game.getEntity(entityTarget.getId()).getPosition());
                hex = game.getBoard().getHex(game.getEntity(entityTarget.getId()).getPosition());
            }
        }

        boolean isAboveWoods = ((entityTarget != null) && (hex != null)) && (entityTarget.absHeight() >= 2);
        boolean isAboveSmoke = ((entityTarget != null) && (hex != null)) && (entityTarget.absHeight() >= 3);
        ToHitData toHit = new ToHitData();

        // if we have in-building combat, it's a +1
        if (attackerInSameBuilding) {
            toHit.addModifier(1, "target in a building hex");
        }

        // Smoke and woods. With L3, the effects STACK.
        int woodsLevel = hex.terrainLevel(Terrains.WOODS);
        int jungleLevel = hex.terrainLevel(Terrains.JUNGLE);
        String woodsText = "woods";
        if (woodsLevel < jungleLevel) {
            woodsLevel = jungleLevel;
            woodsText = "jungle";
        }
        if (woodsLevel == 1) {
            woodsText = "target in light " + woodsText;
        } else if (woodsLevel == 2) {
            woodsText = "target in heavy " + woodsText;
        } else if (woodsLevel == 3) {
            woodsText = "target in ultra heavy " + woodsText;
        }

        if (!game.getOptions().booleanOption("tacops_woods_cover") && !isAboveWoods && !((t.getTargetType() == Targetable.TYPE_HEX_CLEAR) || (t.getTargetType() == Targetable.TYPE_HEX_IGNITE) || (t.getTargetType() == Targetable.TYPE_HEX_BOMB) || (t.getTargetType() == Targetable.TYPE_HEX_ARTILLERY) || (t.getTargetType() == Targetable.TYPE_MINEFIELD_DELIVER))) {
            if ((woodsLevel == 1) && (eistatus != 2)) {
                toHit.addModifier(1, woodsText);
            } else if (woodsLevel > 1) {
                if (eistatus > 0) {
                    toHit.addModifier(woodsLevel - 1, woodsText);
                } else {
                    toHit.addModifier(woodsLevel, woodsText);
                }
            }
        }
        if (!isAboveSmoke) {
            if (hex.terrainLevel(Terrains.SMOKE) == 1) {
                toHit.addModifier(1, "target in light smoke");
            } else if (hex.terrainLevel(Terrains.SMOKE) > 1) {
                if (eistatus > 0) {
                    toHit.addModifier(1, "target in heavy smoke");
                } else {
                    toHit.addModifier(2, "target in heavy smoke");
                }
            }

        }
        if (hex.terrainLevel(Terrains.GEYSER) == 2) {
            if (eistatus > 0) {
                toHit.addModifier(1, "target in erupting geyser");
            } else {
                toHit.addModifier(2, "target in erupting geyser");
            }
        }

        if(hex.containsTerrain(Terrains.INDUSTRIAL)) {
            toHit.addModifier(+1, "target in heavy industrial zone");
        }
        //space screens; bonus depends on number (level)
        if(hex.terrainLevel(Terrains.SCREEN) > 0) {
            toHit.addModifier(hex.terrainLevel(Terrains.SCREEN) + 1, "target in screen(s)");
        }

        // only entities get remaining terrain bonuses
        // TODO: should this be changed for buildings???
        if (entityTarget == null) {
            return toHit;
        } else if (entityTarget.isMakingDfa()) {
            // you don't get terrain modifiers in midair
            // should be abstracted more into a 'not on the ground'
            // flag for vtols and such
            return toHit;
        }

        if (entityTarget.isStuck()) {
            toHit.addModifier(-2, "target stuck in swamp");
        }
        if((entityTarget instanceof Infantry) && hex.containsTerrain(Terrains.FIELDS)) {
            toHit.addModifier(+1, "target in planted fields");
        }
        return toHit;
    }

    /**
     * Returns the weapon attack out of a list that has the highest expected
     * damage
     */
    public static WeaponAttackAction getHighestExpectedDamage(IGame g, Vector<WeaponAttackAction> vAttacks, boolean assumeHit) {
        float fHighest = -1.0f;
        WeaponAttackAction waaHighest = null;
        for (int x = 0, n = vAttacks.size(); x < n; x++) {
            WeaponAttackAction waa = vAttacks.elementAt(x);
            float fDanger = Compute.getExpectedDamage(g, waa, assumeHit);
            if (fDanger > fHighest) {
                fHighest = fDanger;
                waaHighest = waa;
            }
        }
        return waaHighest;
    }

    // store these as constants since the tables will never change
    private static float[] expectedHitsByRackSize = { 0.0f, 1.0f, 1.58f, 2.0f, 2.63f, 3.17f, 4.0f, 4.49f, 4.98f, 5.47f, 6.31f, 7.23f, 8.14f, 8.59f, 9.04f, 9.5f, 10.1f, 10.8f, 11.42f, 12.1f, 12.7f };

    /*
     * | No Modifier | +2 (Artemis, Narc) | -2 (HAG, AMS v Art)| -4 (AMS) | |
     * Avg | Avg | Avg | Avg | | Hits Pct | Hits Pct | Hits Pct | Hits Pct | |
     * Avg Per vs | Avg Per vs | Avg Per vs | Avg Per vs | Size| Hits Size Avg |
     * Hits Size Avg | Hits Size Avg | Hits Size Avg |
     * ----+--------------------+--------------------+--------------------+--------------------+
     * 2 | 1.42 0.708 9.1 | 1.72 0.861 10.3 | 1.17 0.583 10.7 | 1.03 0.514 21.9 |
     * 3 | 2.00 0.667 2.7 | 2.39 0.796 2.0 | 1.61 0.537 2.0 | 1.28 0.426 1.0 | 4 |
     * 2.64 0.660 1.6 | 3.11 0.778 -0.4 | 2.11 0.528 0.2 | 1.67 0.417 -1.2 | 5 |
     * 3.17 0.633 -2.5 | 3.83 0.767 -1.8 | 2.50 0.500 -5.1 | 1.86 0.372 -11.7 |
     * 6 | 4.00 0.667 2.7 | 4.78 0.796 2.0 | 3.22 0.537 2.0 | 2.58 0.431 2.1 | 7 |
     * 4.39 0.627 -3.4 | 5.42 0.774 -0.9 | 3.47 0.496 -5.8 | 2.69 0.385 -8.7 | 8 |
     * 5.08 0.635 -2.1 | 6.06 0.757 -3.0 | 4.22 0.528 0.2 | 3.58 0.448 6.2 | 9 |
     * 5.47 0.608 -6.4 | 6.69 0.744 -4.7 | 4.47 0.497 -5.7 | 3.69 0.410 -2.7 |
     * 10 | 6.31 0.631 -2.9 | 7.67 0.767 -1.8 | 5.06 0.506 -4.0 | 3.97 0.397
     * -5.8 | 11 | 7.31 0.664 2.3 | 8.67 0.788 0.9 | 6.06 0.551 4.5 | 4.97 0.452
     * 7.2 | 12 | 8.14 0.678 4.5 | 9.64 0.803 2.9 | 6.64 0.553 5.0 | 5.25 0.438
     * 3.7 | 13 | 8.42 0.647 -0.3 | 10.22 0.786 0.7 | 6.72 0.517 -1.8 | 5.25
     * 0.404 -4.2 | 14 | 9.22 0.659 1.5 | 10.92 0.780 -0.1 | 7.64 0.546 3.6 |
     * 6.25 0.446 5.9 | 15 | 9.50 0.633 -2.5 | 11.50 0.767 -1.8 | 7.72 0.515
     * -2.3 | 6.25 0.417 -1.2 | 16 | 10.42 0.651 0.3 | 12.50 0.781 0.1 | 8.44
     * 0.528 0.2 | 6.67 0.417 -1.2 | 17 | 10.69 0.629 -3.1 | 13.08 0.770 -1.4 |
     * 8.53 0.502 -4.8 | 6.67 0.392 -7.0 | 18 | 11.50 0.639 -1.6 | 13.78 0.765
     * -1.9 | 9.44 0.525 -0.4 | 7.67 0.426 1.0 | 19 | 11.78 0.620 -4.5 | 14.36
     * 0.756 -3.2 | 9.53 0.501 -4.8 | 7.67 0.404 -4.3 | 20 | 12.69 0.635 -2.2 |
     * 15.36 0.768 -1.6 | 10.25 0.512 -2.7 | 8.08 0.404 -4.2 | 21 | 13.61 0.648
     * -0.2 | 16.33 0.778 -0.4 | 11.11 0.529 0.4 | 8.94 0.426 1.0 | 22 | 14.44
     * 0.657 1.1 | 17.31 0.787 0.8 | 11.69 0.532 0.9 | 9.22 0.419 -0.6 | 23 |
     * 15.36 0.668 2.9 | 18.31 0.796 2.0 | 12.42 0.540 2.5 | 9.64 0.419 -0.6 |
     * 24 | 16.28 0.678 4.5 | 19.28 0.803 2.9 | 13.28 0.553 5.0 | 10.50 0.438
     * 3.7 | 25 | 16.56 0.662 2.0 | 19.86 0.794 1.8 | 13.36 0.534 1.5 | 10.50
     * 0.420 -0.4 | 26 | 17.36 0.668 2.8 | 20.56 0.791 1.3 | 14.28 0.549 4.2 |
     * 11.50 0.442 4.9 | 27 | 17.64 0.653 0.6 | 21.14 0.783 0.3 | 14.36 0.532
     * 1.0 | 11.50 0.426 1.0 | 28 | 17.92 0.640 -1.4 | 21.72 0.776 -0.6 | 14.44
     * 0.516 -2.1 | 11.50 0.411 -2.6 | 29 | 18.72 0.646 -0.6 | 22.42 0.773 -1.0 |
     * 15.36 0.530 0.6 | 12.50 0.431 2.2 | 30 | 19.00 0.633 -2.5 | 23.00 0.767
     * -1.8 | 15.44 0.515 -2.3 | 12.50 0.417 -1.2 | 40 | 25.39 0.635 -2.2 |
     * 30.72 0.768 -1.6 | 20.50 0.512 -2.7 | 16.17 0.404 -4.2 | ----- -----
     * ----- ----- Average: 0.649 0.781 0.527 0.422 1.202 0.811 0.649
     */

    /**
     * Determines the expected damage of a weapon attack, based on to-hit, salvo
     * sizes, etc.
     */
    public static float getExpectedDamage(IGame g, WeaponAttackAction waa, boolean assumeHit) {
        boolean use_table = false;

        AmmoType loaded_ammo = new AmmoType();

        Entity attacker = g.getEntity(waa.getEntityId());
        Infantry inf_attacker = new Infantry();
        BattleArmor ba_attacker = new BattleArmor();
        Mounted weapon = attacker.getEquipment(waa.getWeaponId());
        Mounted lnk_guide;

        ToHitData hitData = waa.toHit(g);

        if (attacker instanceof BattleArmor) {
            ba_attacker = (BattleArmor) g.getEntity(waa.getEntityId());
        }
        if ((attacker instanceof Infantry) && !(attacker instanceof BattleArmor)) {
            inf_attacker = (Infantry) g.getEntity(waa.getEntityId());
        }

        WeaponType wt = (WeaponType) weapon.getType();

        float fDamage = 0.0f;
        float fChance = 0.0f;
        if (assumeHit) {
            fChance = 1.0f;
        } else {
            if ((hitData.getValue() == TargetRoll.IMPOSSIBLE) || (hitData.getValue() == TargetRoll.AUTOMATIC_FAIL)) {
                return 0.0f;
            }

            if (hitData.getValue() == TargetRoll.AUTOMATIC_SUCCESS) {
                fChance = 1.0f;
            } else {
                fChance = (float) Compute.oddsAbove(hitData.getValue()) / 100.0f;
            }
        }

        // Missiles, LBX cluster rounds, and ultra/rotary cannons (when spun up)
        // use the missile hits table
        if (wt.getDamage() == WeaponType.DAMAGE_MISSILE) {
            use_table = true;
        }
        if ((wt.getAmmoType() == AmmoType.T_AC_LBX) || (wt.getAmmoType() == AmmoType.T_AC_LBX_THB) || (wt.getAmmoType() == AmmoType.T_AC_LBX_THB)) {
            loaded_ammo = (AmmoType) weapon.getLinked().getType();
            if (((loaded_ammo.getAmmoType() == AmmoType.T_AC_LBX) || (loaded_ammo.getAmmoType() == AmmoType.T_AC_LBX_THB)) && (loaded_ammo.getMunitionType() == AmmoType.M_CLUSTER)) {
                use_table = true;
            }
        }

        if ((wt.getAmmoType() == AmmoType.T_AC_ULTRA) || (wt.getAmmoType() == AmmoType.T_AC_ULTRA_THB) || (wt.getAmmoType() == AmmoType.T_AC_ROTARY)) {
            if ((weapon.curMode().getName() == "Ultra") || (weapon.curMode().getName() == "2-shot") || (weapon.curMode().getName() == "3-shot") || (weapon.curMode().getName() == "4-shot") || (weapon.curMode().getName() == "5-shot") || (weapon.curMode().getName() == "6-shot")) {
                use_table = true;
            }
        }

        // Kinda cheap, but lets use the missile hits table for Battle armor
        // weapons too

        if (attacker instanceof BattleArmor) {
            if ((wt.getInternalName() != Infantry.SWARM_MEK) && (wt.getInternalName() != Infantry.LEG_ATTACK)) {
                use_table = true;
            }
        }

        if (use_table == true) {
            if (!(attacker instanceof BattleArmor)) {
                if (weapon.getLinked() == null) {
                    return 0.0f;
                }
            }
            AmmoType at = null;
            if (weapon.getLinked() != null) {
                at = (AmmoType) weapon.getLinked().getType();
                fDamage = at.getDamagePerShot();
            }

            float fHits = 0.0f;
            if ((wt.getRackSize() != 40) && (wt.getRackSize() != 30)) {
                fHits = expectedHitsByRackSize[wt.getRackSize()];
            } else {
                fHits = 2.0f * expectedHitsByRackSize[wt.getRackSize() / 2];
            }
            if (((wt.getAmmoType() == AmmoType.T_SRM_STREAK) || (wt.getAmmoType() == AmmoType.T_MRM_STREAK) || (wt.getAmmoType() == AmmoType.T_LRM_STREAK)) && !Compute.isAffectedByAngelECM(attacker, attacker.getPosition(), waa.getTarget(g).getPosition())) {
                fHits = wt.getRackSize();
            }
            if ((wt.getAmmoType() == AmmoType.T_AC_ULTRA) || (wt.getAmmoType() == AmmoType.T_AC_ULTRA_THB) || (wt.getAmmoType() == AmmoType.T_AC_ROTARY)) {
                if ((weapon.curMode().getName() == "Ultra") || (weapon.curMode().getName() == "2-shot")) {
                    fHits = expectedHitsByRackSize[2];
                }
                if (weapon.curMode().getName() == "3-shot") {
                    fHits = expectedHitsByRackSize[3];
                }
                if (weapon.curMode().getName() == "4-shot") {
                    fHits = expectedHitsByRackSize[4];
                }
                if (weapon.curMode().getName() == "5-shot") {
                    fHits = expectedHitsByRackSize[5];
                }
                if (weapon.curMode().getName() == "6-shot") {
                    fHits = expectedHitsByRackSize[6];
                }
            }

            // Most Battle Armor units have a weapon per trooper, plus their
            // weapons do odd things when mounting multiples
            if (attacker instanceof BattleArmor) {
                // The number of troopers hitting
                fHits = expectedHitsByRackSize[ba_attacker.getShootingStrength()];
                if (wt.getDamage() == WeaponType.DAMAGE_MISSILE) {
                    fHits *= expectedHitsByRackSize[wt.getRackSize()];
                }
                if (wt.getDamage() != WeaponType.DAMAGE_MISSILE) {
                    if (wt.getDamage() != WeaponType.DAMAGE_VARIABLE) {
                        fDamage = wt.getDamage();
                    } else {
                        fDamage = wt.getRackSize();
                    }
                }
                if (wt.hasFlag(WeaponType.F_MISSILE_HITS)) {
                    fHits *= expectedHitsByRackSize[wt.getRackSize()];
                }
            }

            // If there is no ECM coverage to the target, guidance systems are
            // good for another 1.20x damage on missile weapons
            if ((!Compute.isAffectedByECM(attacker, attacker.getPosition(), g.getEntity(waa.getTargetId()).getPosition())) && (wt.getDamage() == WeaponType.DAMAGE_MISSILE)) {
                // Check for linked artemis guidance system
                if ((wt.getAmmoType() == AmmoType.T_LRM) || (wt.getAmmoType() == AmmoType.T_MML) || (wt.getAmmoType() == AmmoType.T_SRM)) {
                    lnk_guide = weapon.getLinkedBy();
                    if ((lnk_guide != null) && (lnk_guide.getType() instanceof MiscType) && !lnk_guide.isDestroyed() && !lnk_guide.isMissing() && !lnk_guide.isBreached() && lnk_guide.getType().hasFlag(MiscType.F_ARTEMIS)) {

                        // Don't use artemis if this is indirect fire
                        // -> Hook for Artemis V Level 3 Clan tech here; use
                        // 1.30f multiplier when implemented
                        if (((weapon.curMode() == null) || !weapon.curMode().equals("Indirect")) && (at.getMunitionType() == AmmoType.M_ARTEMIS_CAPABLE)) {
                            fHits *= 1.2f;
                        }
                    }
                }

                // Check for ATMs, which have built in Artemis
                if (wt.getAmmoType() == AmmoType.T_ATM) {
                    fHits *= 1.2f;
                }

                // Check for target with attached Narc or iNarc homing pod from
                // friendly unit
                if (g.getEntity(waa.getTargetId()).isNarcedBy(attacker.getOwner().getTeam()) || g.getEntity(waa.getTargetId()).isINarcedBy(attacker.getOwner().getTeam())) {
                    if (((at.getAmmoType() == AmmoType.T_LRM) || (at.getAmmoType() == AmmoType.T_MML) || (at.getAmmoType() == AmmoType.T_SRM)) && (at.getMunitionType() == AmmoType.M_NARC_CAPABLE)) {
                        fHits *= 1.2f;
                    }
                }
            }

            if (wt.getAmmoType() == AmmoType.T_MRM) {
                lnk_guide = weapon.getLinkedBy();
                if ((lnk_guide != null) && (lnk_guide.getType() instanceof MiscType) && !lnk_guide.isDestroyed() && !lnk_guide.isMissing() && !lnk_guide.isBreached() && lnk_guide.getType().hasFlag(MiscType.F_APOLLO)) {
                    fHits *= .9f;
                }
            }


            // adjust for previous AMS
            if (wt.getDamage() == WeaponType.DAMAGE_MISSILE) {
                ArrayList<Mounted> vCounters = waa.getCounterEquipment();
                if (vCounters != null) {
                    for (int x = 0; x < vCounters.size(); x++) {
                        EquipmentType type = vCounters.get(x).getType();
                        if ((type instanceof WeaponType) && type.hasFlag(WeaponType.F_AMS)) {
                            fHits *= 0.6;
                        }
                    }
                }
            }

            fDamage *= fHits;

            if ((wt.getAmmoType() == AmmoType.T_AC_ULTRA) || (wt.getAmmoType() == AmmoType.T_AC_ULTRA_THB) || (wt.getAmmoType() == AmmoType.T_AC_ROTARY)) {
                fDamage = fHits * wt.getDamage();
            }

        } else {
            // Direct fire weapons (and LBX slug rounds) just do a single shot
            // so they don't use the missile hits table
            fDamage = wt.getDamage();
            if ((attacker.getPosition() != null) && (g.getEntity(waa.getTargetId()).getPosition() != null)) {
                if (wt.getAmmoType() == AmmoType.T_GAUSS_HEAVY) {
                    fDamage = 25.0f;
                    int rtt = attacker.getPosition().distance(g.getEntity(waa.getTargetId()).getPosition());
                    if (rtt > 13) {
                        fDamage = 10.0f;
                    } else if (rtt > 6) {
                        fDamage = 20.0f;
                    }
                }
            }

            // Infantry follow some special rules, but do fixed amounts of
            // damage
            // Anti-mek attacks are weapon-like in nature, so include them here
            // as well
            if (attacker instanceof Infantry) {
                if (wt.getInternalName() == Infantry.LEG_ATTACK) {
                    fDamage = 10.0f; // Actually 5, but the chance of crits
                    // deserves a boost
                }

                if (inf_attacker.isPlatoon()) {
                    if (wt.getInternalName() == Infantry.SWARM_MEK) {
                        // If the target is a Mek that is not swarmed, this is a
                        // good thing
                        if ((g.getEntity(waa.getTargetId()).getSwarmAttackerId() == Entity.NONE) && (g.getEntity(waa.getTargetId()) instanceof Mech)) {
                            /*
                             * fDamage = 1.5f * inf_attacker
                             * .getDamage(inf_attacker .getShootingStrength());
                             */
                            // TODO: Fix me
                            fDamage = 4;
                        }
                        // Otherwise, call it 0 damage
                        else {
                            fDamage = 0.0f;
                        }
                    }

                    else {
                        // conventional weapons; field guns should be handled
                        // under the standard weapons section
                        /*
                         * fDamage = 0.6f * inf_attacker.getDamage(inf_attacker
                         * .getShootingStrength());
                         */
                        // TODO: Fix me
                        fDamage = 2;
                    }

                } else {
                    // Battle armor units conducting swarm attack
                    if (wt.getInternalName() == Infantry.SWARM_MEK) {
                        // If the target is a Mek that is not swarmed, this is a
                        // good thing
                        if ((g.getEntity(waa.getTargetId()).getSwarmAttackerId() == Entity.NONE) && (g.getEntity(waa.getTargetId()) instanceof Mech)) {
                            // Overestimated, but the chance at crits and head
                            // shots deserves a boost
                            fDamage = 5.0f * ba_attacker.getShootingStrength();
                        }
                        // Otherwise, call it 0 damage
                        else {
                            fDamage = 0.0f;
                        }
                    }

                }
            }

        }
        fDamage *= fChance;

        // Conventional infantry take double damage in the open
        if ((g.getEntity(waa.getTargetId()) instanceof Infantry) && !(g.getEntity(waa.getTargetId()) instanceof BattleArmor)) {
            IHex e_hex = g.getBoard().getHex(g.getEntity(waa.getTargetId()).getPosition().x, g.getEntity(waa.getTargetId()).getPosition().y);
            if (!e_hex.containsTerrain(Terrains.WOODS) && !e_hex.containsTerrain(Terrains.JUNGLE) && !e_hex.containsTerrain(Terrains.BUILDING)) {
                fDamage *= 2.0f;
            }

            // Cap damage to prevent run-away values
            fDamage = Math.min(inf_attacker.getShootingStrength(), fDamage);
        }
        return fDamage;
    }

    /**
     * If the unit is carrying multiple types of ammo for the specified weapon,
     * cycle through them and choose the type best suited to engage the
     * specified target Value returned is expected damage Note that some ammo
     * types, such as infernos, do no damage or have special properties and so
     * the damage is an estimation of effectiveness
     */

    public static double getAmmoAdjDamage(IGame cgame, WeaponAttackAction atk) {
        boolean no_bin = true;
        boolean multi_bin = false;

        double ammo_multiple, ex_damage, max_damage;

        Entity shooter, target;

        Mounted fabin, best_bin;
        AmmoType abin_type = new AmmoType();
        AmmoType fabin_type = new AmmoType();
        WeaponType wtype = new WeaponType();
        WeaponType target_weapon = new WeaponType();

        // Get shooter entity, target entity, and weapon being fired
        target = cgame.getEntity(atk.getTargetId());
        shooter = atk.getEntity(cgame);
        wtype = (WeaponType) shooter.getEquipment(atk.getWeaponId()).getType();

        max_damage = 0.0;

        // If the weapon doesn't require ammo, just get the estimated damage
        if (wtype.hasFlag(WeaponType.F_ENERGY) || wtype.hasFlag(WeaponType.F_ONESHOT) || wtype.hasFlag(WeaponType.F_INFANTRY) || (wtype.getAmmoType() == AmmoType.T_NA)) {
            return Compute.getExpectedDamage(cgame, atk, false);
        }

        // Get a list of ammo bins and the first valid bin
        fabin = null;
        best_bin = null;

        for (Mounted abin : shooter.getAmmo()) {
            if (shooter.loadWeapon(shooter.getEquipment(atk.getWeaponId()), abin)) {
                if (abin.getShotsLeft() > 0) {
                    abin_type = (AmmoType) abin.getType();
                    if (!AmmoType.canDeliverMinefield(abin_type)) {
                        fabin = abin;
                        fabin_type = (AmmoType) fabin.getType();
                        break;
                    }
                }
            }
        }

        // To save processing time, lets see if we have more than one type of
        // bin
        // Thunder-type ammos and empty bins are excluded from the list
        for (Mounted abin : shooter.getAmmo()) {
            if (shooter.loadWeapon(shooter.getEquipment(atk.getWeaponId()), abin)) {
                if (abin.getShotsLeft() > 0) {
                    abin_type = (AmmoType) abin.getType();
                    if (!AmmoType.canDeliverMinefield(abin_type)) {
                        no_bin = false;
                        if (abin_type.getMunitionType() != fabin_type.getMunitionType()) {
                            multi_bin = true;
                            break;
                        }
                    }
                }
            }
        }

        // If no_bin is true, then either all bins are empty or contain
        // Thunder-type rounds and
        // we can safely say that the expected damage is 0.0
        // If no_bin is false, then we have at least one good bin
        if (no_bin) {
            return 0.0;
        }
        // If multi_bin is true, then multiple ammo types are present and an
        // appropriate type must be selected
        // If multi_bin is false, then all bin types are the same; skip down
        // to getting the expected damage
        if (!multi_bin) {
            return Compute.getExpectedDamage(cgame, atk, false);
        }
        if (multi_bin) {

            // Set default max damage as 0, and the best bin as the first
            // bin
            max_damage = 0.0;
            best_bin = fabin;

            // For each valid ammo bin
            for (Mounted abin : shooter.getAmmo()) {
                if (shooter.loadWeapon(shooter.getEquipment(atk.getWeaponId()), abin)) {
                    if (abin.getShotsLeft() > 0) {
                        abin_type = (AmmoType) abin.getType();
                        if (!AmmoType.canDeliverMinefield(abin_type)) {

                            // Load weapon with specified bin
                            shooter.loadWeapon(shooter.getEquipment(atk.getWeaponId()), abin);
                            atk.setAmmoId(shooter.getEquipmentNum(abin));

                            // Get expected damage
                            ex_damage = Compute.getExpectedDamage(cgame, atk, false);

                            // Calculate any modifiers due to ammo type
                            ammo_multiple = 1.0;

                            // Frag missiles, flechette AC rounds do double
                            // damage against conventional infantry
                            // and 0 damage against everything else
                            // Any further anti-personnel specialized rounds
                            // should be tested for here
                            if (((((abin_type.getAmmoType() == AmmoType.T_LRM) || (abin_type.getAmmoType() == AmmoType.T_MML) || (abin_type.getAmmoType() == AmmoType.T_SRM))) && (abin_type.getMunitionType() == AmmoType.M_FRAGMENTATION)) || (((abin_type.getAmmoType() == AmmoType.T_AC) || (abin_type.getAmmoType() == AmmoType.T_LAC)) && (abin_type.getMunitionType() == AmmoType.M_FLECHETTE))) {
                                ammo_multiple = 0.0;
                                if (target instanceof Infantry) {
                                    if (!(target instanceof BattleArmor)) {
                                        ammo_multiple = 2.0;
                                    }
                                }
                            }

                            // LBX cluster rounds work better against units
                            // with little armor, vehicles, and Meks in
                            // partial cover
                            // Other ammo that deliver lots of small
                            // submunitions should be tested for here too
                            if (((abin_type.getAmmoType() == AmmoType.T_AC_LBX) || (abin_type.getAmmoType() == AmmoType.T_AC_LBX_THB) || (abin_type.getAmmoType() == AmmoType.T_SBGAUSS)) && (abin_type.getMunitionType() == AmmoType.M_CLUSTER)) {
                                if (target.getArmorRemainingPercent() <= 0.25) {
                                    ammo_multiple = 1.0 + (wtype.getRackSize() / 10);
                                }
                                if (target instanceof Tank) {
                                    ammo_multiple += 1.0;
                                }
                            }

                            // AP autocannon rounds work much better against
                            // Meks and vehicles than infantry,
                            // give a damage boost in proportion to calibre
                            // to reflect scaled crit chance
                            // Other armor-penetrating ammo types should be
                            // tested here, such as Tandem-charge SRMs
                            if (((abin_type.getAmmoType() == AmmoType.T_AC) || (abin_type.getAmmoType() == AmmoType.T_LAC)) && (abin_type.getMunitionType() == AmmoType.M_ARMOR_PIERCING)) {
                                if ((target instanceof Mech) || (target instanceof Tank)) {
                                    ammo_multiple = 1.0 + (wtype.getRackSize() / 10);
                                }
                                if (target instanceof Infantry) {
                                    ammo_multiple = 0.6;
                                }
                            }

                            // Inferno SRMs work better against overheating
                            // Meks that are not/almost not on fire,
                            // and against vehicles and protos if allowed by
                            // game option
                            if (((abin_type.getAmmoType() == AmmoType.T_SRM) || (abin_type.getAmmoType() == AmmoType.T_MML)) && (abin_type.getMunitionType() == AmmoType.M_INFERNO)) {
                                ammo_multiple = 0.5;
                                if (target instanceof Mech) {
                                    if ((target.infernos.getTurnsLeftToBurn() < 4) && (target.heat >= 5)) {
                                        ammo_multiple = 1.1;
                                    }
                                }
                                if ((target instanceof Tank) && !(cgame.getOptions().booleanOption("vehicles_safe_from_infernos"))) {
                                    ammo_multiple = 1.1;
                                }
                                if ((target instanceof Protomech) && !(cgame.getOptions().booleanOption("protos_safe_from_infernos"))) {
                                    ammo_multiple = 1.1;
                                }
                            }

                            // Narc beacon doesn't really do damage but if
                            // the target is not infantry and doesn't have
                            // one, give 'em one by making it an attractive
                            // option
                            if ((wtype.getAmmoType() == AmmoType.T_NARC) && (abin_type.getMunitionType() == AmmoType.M_STANDARD)) {
                                if (!(target.isNarcedBy(shooter.getOwner().getTeam())) && !(target instanceof Infantry)) {
                                    ex_damage = 5.0;
                                } else {
                                    ex_damage = 0.5;
                                }
                            }

                            // iNarc beacon doesn't really do damage, but if
                            // the target is not infantry and doesn't have
                            // one, give 'em one by making it an attractive
                            // option
                            if (wtype.getAmmoType() == AmmoType.T_INARC) {
                                if ((abin_type.getMunitionType() == AmmoType.M_STANDARD) && !(target instanceof Infantry)) {
                                    if (!(target.isINarcedBy(shooter.getOwner().getTeam()))) {
                                        ex_damage = 7.0;
                                    } else {
                                        ex_damage = 1.0;
                                    }
                                }

                                // iNarc ECM doesn't really do damage, but
                                // if the target has a C3 link or missile
                                // launchers
                                // make it a priority
                                // Checking for actual ammo types carried
                                // would be nice, but can't be sure of exact
                                // loads
                                // when "true" double blind is implemented
                                if ((abin_type.getAmmoType() == AmmoType.T_INARC) && (abin_type.getMunitionType() == AmmoType.M_ECM) && !(target instanceof Infantry)) {
                                    if (!target.isINarcedWith(AmmoType.M_ECM)) {
                                        if (!(target.getC3MasterId() == Entity.NONE) || target.hasC3M() || target.hasC3MM() || target.hasC3i()) {
                                            ex_damage = 8.0;
                                        } else {
                                            ex_damage = 0.5;
                                        }
                                        for (Mounted weapon : shooter.getWeaponList()) {
                                            target_weapon = (WeaponType) weapon.getType();
                                            if ((target_weapon.getAmmoType() == AmmoType.T_LRM) || (target_weapon.getAmmoType() == AmmoType.T_MML) || (target_weapon.getAmmoType() == AmmoType.T_SRM)) {
                                                ex_damage = ex_damage + (target_weapon.getRackSize() / 2);
                                            }
                                        }
                                    }
                                }

                                // iNarc Nemesis doesn't really do damage,
                                // but if the target is not infantry and
                                // doesn't have
                                // one give it a try; make fast units a
                                // priority because they are usually out
                                // front
                                if ((abin_type.getAmmoType() == AmmoType.T_INARC) && (abin_type.getMunitionType() == AmmoType.M_NEMESIS) && !(target instanceof Infantry)) {
                                    if (!target.isINarcedWith(AmmoType.M_NEMESIS)) {
                                        ex_damage = (double) (target.getWalkMP() + target.getJumpMP()) / 2;
                                    } else {
                                        ex_damage = 0.5;
                                    }
                                }
                            }

                            // If the adjusted damage is highest, store the
                            // damage and bin
                            if ((ex_damage * ammo_multiple) > max_damage) {
                                max_damage = ex_damage * ammo_multiple;
                                best_bin = abin;
                            }
                        }
                    }
                }
            }

            // Now that the best bin has been found, reload the weapon with
            // it
            shooter.loadWeapon(shooter.getEquipment(atk.getWeaponId()), best_bin);
            atk.setAmmoId(shooter.getEquipmentNum(best_bin));
        }
        return max_damage;
    }

    /**
     * If this is an ultra or rotary cannon, lets see about 'spinning it up' for
     * extra damage
     *
     * @return the <code>int</code> ID of weapon mode
     */

    public static int spinUpCannon(IGame cgame, WeaponAttackAction atk) {

        int threshold = 12;
        int test, final_spin;
        Entity shooter;
        Mounted weapon;
        WeaponType wtype = new WeaponType();

        // Double check this is an Ultra or Rotary cannon
        shooter = atk.getEntity(cgame);
        weapon = shooter.getEquipment(atk.getWeaponId());
        wtype = (WeaponType) shooter.getEquipment(atk.getWeaponId()).getType();

        if (!((wtype.getAmmoType() == AmmoType.T_AC_ULTRA) || (wtype.getAmmoType() == AmmoType.T_AC_ULTRA_THB) || (wtype.getAmmoType() == AmmoType.T_AC_ROTARY))) {
            return 0;
        }

        // Get the to-hit number
        threshold = atk.toHit(cgame).getValue();

        // Set the weapon to single shot mode
        weapon.setMode("Single");
        final_spin = 0;

        // If weapon can't hit target, exit the function with the weapon on
        // single shot
        if ((threshold == TargetRoll.IMPOSSIBLE) || (threshold == TargetRoll.AUTOMATIC_FAIL)) {
            return final_spin;
        }

        // Set a random 2d6 roll
        test = Compute.d6(2);

        // If random roll is >= to-hit + 1, then set double-spin
        if (test >= threshold + 1) {
            final_spin = 1;
            if ((wtype.getAmmoType() == AmmoType.T_AC_ULTRA) || (wtype.getAmmoType() == AmmoType.T_AC_ULTRA_THB)) {
                weapon.setMode("Ultra");
            }
            if (wtype.getAmmoType() == AmmoType.T_AC_ROTARY) {
                weapon.setMode("2-shot");
            }
        }

        // If this is a Rotary cannon
        if (wtype.getAmmoType() == AmmoType.T_AC_ROTARY) {

            // If random roll is >= to-hit + 2 then set to quad-spin
            if (test >= threshold + 2) {
                final_spin = 2;
                weapon.setMode("4-shot");
            }

            // If random roll is >= to-hit + 3 then set to six-spin
            if (test >= threshold + 3) {
                final_spin = 3;
                weapon.setMode("6-shot");
            }
        }
        return final_spin;
    }

    /**
     * Checks to see if a target is in arc of the specified weapon, on the
     * specified entity
     */
    public static boolean isInArc(IGame game, int attackerId, int weaponId, Targetable t) {
        Entity ae = game.getEntity(attackerId);
        if ((ae instanceof Mech) && (((Mech) ae).getGrappled() == t.getTargetId())) {
            return true;
        }
        int facing = ae.isSecondaryArcWeapon(weaponId) ? ae.getSecondaryFacing() : ae.getFacing();
        Coords aPos = ae.getPosition();
        Coords tPos = t.getPosition();

        //aeros in the same hex in space may still be able to fire at one another. First I need to translate
        //their positions to see who was further back
        if(game.getBoard().inSpace() && ae.getPosition().equals(t.getPosition())
                && (ae instanceof Aero) && (t instanceof Aero)) {
            if(((Aero)ae).shouldMoveBackHex((Aero)t)) {
                aPos = ae.getPriorPosition();
            }
            if(((Aero)t).shouldMoveBackHex((Aero)ae)) {
                tPos = ((Entity)t).getPosition();
            }
        }
        return Compute.isInArc(aPos, facing, tPos, ae.getWeaponArc(weaponId));
    }

    /**
     * Returns true if the line between source Coords and target goes through
     * the hex in front of the attacker
     */
    public static boolean isThroughFrontHex(IGame game, Coords src, Entity t) {
        Coords dest = t.getPosition();
        int fa = dest.degree(src) - t.getFacing() * 60;
        if (fa < 0) {
            fa += 360;
        }
        return (fa > 330) || (fa < 30);
    }

    /**
     * Returns true if the target is in the specified arc.
     *
     * @param src
     *            the attacker coordinate
     * @param facing
     *            the appropriate attacker sfacing
     * @param dest
     *            the target coordinate
     * @param arc
     *            the arc
     */
    public static boolean isInArc(Coords src, int facing, Coords dest, int arc) {
        if ((src == null) || (dest == null)) {
            return true;
        }
        // calculate firing angle
        int fa = src.degree(dest) - facing * 60;
        if (fa < 0) {
            fa += 360;
        }
        // is it in the specifed arc?
        switch (arc) {
        case ARC_FORWARD:
            return (fa >= 300) || (fa <= 60);
        case Compute.ARC_RIGHTARM:
            return (fa >= 300) || (fa <= 120);
        case Compute.ARC_LEFTARM:
            return (fa >= 240) || (fa <= 60);
        case ARC_REAR:
            return (fa > 120) && (fa < 240);
        case ARC_RIGHTSIDE:
            return (fa > 60) && (fa <= 120);
        case ARC_LEFTSIDE:
            return (fa < 300) && (fa >= 240);
        case ARC_MAINGUN:
            return (fa >= 240) || (fa <= 120);
        case ARC_360:
            return true;
        case ARC_NORTH:
            return (fa >= 270) || (fa <= 30);
        case ARC_EAST:
            return (fa >= 30) && (fa <= 150);
        case ARC_WEST:
            return (fa >= 150) && (fa <= 270);
        case ARC_NOSE:
            return (fa > 300) || (fa < 60);
        case ARC_LWING:
            return (fa > 300) || (fa <= 0);
        case ARC_RWING:
            return (fa >= 0) && (fa < 60);
        case ARC_LWINGA:
            return (fa >= 180) && (fa < 240);
        case ARC_RWINGA:
            return (fa > 120) && (fa <= 180);
        case ARC_AFT:
            return (fa > 120) && (fa < 240);
        case ARC_LEFTSIDE_SPHERE:
            return (fa > 240) || (fa < 0);
        case ARC_RIGHTSIDE_SPHERE:
            return (fa > 0) && (fa < 120);
        case ARC_LEFTSIDEA_SPHERE:
            return (fa > 180) && (fa < 300);
        case ARC_RIGHTSIDEA_SPHERE:
            return (fa > 60) && (fa < 180);
        case ARC_LEFT_BROADSIDE:
            return (fa >= 240) && (fa <= 300);
        case ARC_RIGHT_BROADSIDE:
            return (fa >= 60) && (fa <= 120);
        case ARC_LEFT_SPHERE_GROUND:
            return (fa >= 180) && (fa < 360);
        case ARC_RIGHT_SPHERE_GROUND:
            return (fa >= 0) && (fa < 180);
        case ARC_TURRET:
            return (fa >= 330) || (fa <= 30);
        default:
            return false;
        }
    }

    /**
     * checks to see whether the target is within visual range of the entity, but not necessarily LoS
     */
    public static boolean inVisualRange(IGame game, Entity ae, Targetable target) {
        boolean teSpotlight = false;
        if (target.getTargetType() == Targetable.TYPE_ENTITY) {
            Entity te = (Entity) target;
            teSpotlight = te.usedSearchlight();
            if (te.isOffBoard()) {
                return false;
            }
        }

        //if either does not have a position then return false
        if((ae.getPosition() == null) || (target.getPosition() == null)) {
            return false;
        }

        //check visual range based on planetary conditions
        int visualRange = game.getPlanetaryConditions().getVisualRange(ae, teSpotlight);

        //smoke in los
        visualRange -= LosEffects.calculateLos(game, ae.getId(), target).getLightSmoke();
        visualRange -= (2 * LosEffects.calculateLos(game, ae.getId(), target).getHeavySmoke());

        //check for camo and null sig on the target
        if(target.getTargetType() == Targetable.TYPE_ENTITY) {
            Entity te = (Entity) target;

            if ( game.getTeamForPlayer(ae.getOwner()).equals(game.getTeamForPlayer(te.getOwner())) ){
                return true;
            }

            if(te.isVoidSigActive()) {
                visualRange = visualRange / 4;
            } else if(te.hasWorkingMisc(MiscType.F_VISUAL_CAMO, -1)) {
                visualRange = visualRange / 2;
            } else if(te.isChameleonShieldActive()) {
                visualRange = visualRange / 2;
            }
        }

        visualRange = Math.max(visualRange, 1);

        return ae.getPosition().distance(target.getPosition()) <= visualRange;

    }

    /**
     * Checks to see whether the target is within sensor range (but not necessarily LoS or visual range)
     */
    public static boolean inSensorRange(IGame game, Entity ae, Targetable target) {

        //if either does not have a position then return false
        if((ae.getPosition() == null) || (target.getPosition() == null)) {
            return false;
        }

        int bracket = Compute.getSensorRangeBracket(ae, target);
        int range = Compute.getSensorRangeByBracket(game, ae, target);

        int maxSensorRange = bracket*range;
        int minSensorRange = Math.max((bracket-1)*range,0);
        if(game.getOptions().booleanOption("inclusive_sensor_range")) {
            minSensorRange = 0;
        }

        int distance = ae.getPosition().distance(target.getPosition());

        return (distance > minSensorRange) && (distance <= maxSensorRange);
    }

    /**
     * Slightly misnamed. Checks to see if the target is visible to the unit, either visually or through sensors
     */
    public static boolean canSee(IGame game, Entity ae, Targetable target) {

        if(!ae.getCrew().isActive()) {
            return false;
        }
        if (target.isOffBoard()) {
            return false;
        }

        return (LosEffects.calculateLos(game, ae.getId(), target).canSee() && Compute.inVisualRange(game, ae, target)) || Compute.inSensorRange(game, ae, target);
    }

    private static int getSensorRangeBracket(Entity ae, Targetable target) {

        Sensor sensor = ae.getActiveSensor();
        if(null == sensor) {
            return 0;
        }
        //only works for entities
        if(target.getTargetType() != Targetable.TYPE_ENTITY) {
            return 0;
        }
        Entity te = (Entity)target;

        //if this sensor is an active probe and it is critted, then no can see
        if(sensor.isBAP() && !ae.hasBAP(false)) {
            return 0;
        }

        int check = ae.getSensorCheck();
        check += sensor.getModsForStealth(te);
        //ECM bubbles
        check += sensor.getModForECM(ae);

        //get the range bracket (0 - none; 1 - short; 2 - medium; 3 - long)
        int bracket = 0;
        if((check == 7) || (check == 8)) {
            bracket = 1;
        }
        if((check == 5) || (check == 6)) {
            bracket = 2;
        }
        if(check < 5) {
            bracket = 3;
        }

        return bracket;
    }
    /**
     * Checks whether the target is within sensor range of the current entity
     */
    private static int getSensorRangeByBracket(IGame game, Entity ae, Targetable target) {

        Sensor sensor = ae.getActiveSensor();
        if(null == sensor) {
            return 0;
        }
        //only works for entities
        if(target.getTargetType() != Targetable.TYPE_ENTITY) {
            return 0;
        }
        Entity te = (Entity)target;

        //if this sensor is an active probe and it is critted, then no can see
        if(sensor.isBAP() && !ae.hasBAP(false)) {
            return 0;
        }

        //if we are crossing water then only magscan will work unless we are a naval vessel
        if(LosEffects.calculateLos(game, ae.getId(), target).isBlockedByWater()
                && (sensor.getType() != Sensor.TYPE_MEK_MAGSCAN) && (sensor.getType() != Sensor.TYPE_VEE_MAGSCAN)
                && (ae.getMovementMode() != IEntityMovementMode.HYDROFOIL) && (ae.getMovementMode() != IEntityMovementMode.NAVAL)) {
            return 0;
        }

        //now get the range
        int range = sensor.getRangeByBracket();

        //adjust the range based on LOS and planetary conditions
        range = sensor.adjustRange(range, game, LosEffects.calculateLos(game, ae.getId(), target));

        //now adjust for anything about the target entity (size, heat, etc)
        range = sensor.entityAdjustments(range, te, game);

        if(range < 0) {
            range = 0;
        }

        return range;

    }

    public static int targetSideTable(Coords inPosition, Targetable target) {
        return target.sideTable(inPosition);
    }

    public static int targetSideTable(Entity attacker, Targetable target) {
        Coords attackPos = attacker.getPosition();

        boolean usePrior = false;
        //aeros in the same hex in space need to adjust position to get side table
        if(attacker.game.getBoard().inSpace() && attacker.getPosition().equals(target.getPosition())
                && (attacker instanceof Aero) && (target instanceof Aero)) {
            if(((Aero)attacker).shouldMoveBackHex((Aero)target)) {
                attackPos = attacker.getPriorPosition();
            }
            usePrior = ((Aero)target).shouldMoveBackHex((Aero)attacker);
        }
        if((target instanceof Aero) && (attacker instanceof Aero)) {
            return ((Entity)target).sideTable(attackPos, usePrior);
        }
        return target.sideTable(attackPos);
    }

    /**
     * Maintain backwards compatability.
     *
     * @param missiles -
     *            the <code>int</code> number of missiles in the pack.
     */
    public static int missilesHit(int missiles) {
        return Compute.missilesHit(missiles, 0);
    }

    /**
     * Maintain backwards compatability.
     * @param missiles
     * @param nMod
     * @return
     */
    public static int missilesHit(int missiles, int nMod) {
        return Compute.missilesHit(missiles, nMod, false);
    }

    /**
     * Maintain backwards compatability.
     * @param missiles
     * @param nMod
     * @param hotloaded
     * @return
     */
    public static int missilesHit(int missiles, int nMod, boolean hotloaded) {
        return Compute.missilesHit(missiles, nMod, hotloaded, false, false);
    }

    /**
     * Roll the number of missiles (or whatever) on the missile hit table, with
     * the specified mod to the roll.
     *
     * @param missiles -
     *            the <code>int</code> number of missiles in the pack.
     * @param nMod -
     *            the <code>int</code> modifier to the roll for number of
     *            missiles that hit.
     * @param hotloaded -
     *            roll 3d6 take worst 2
     * @param streak -
     *            force a roll of 11 on the cluster table
     * @param advancedAMS -
     *            the roll can now go below 2, indicating no damage
     */
    public static int missilesHit(int missiles, int nMod, boolean hotloaded, boolean streak, boolean advancedAMS) {
        int nRoll = Compute.d6(2);

        if (hotloaded) {
            int roll1 = Compute.d6();
            int roll2 = Compute.d6();
            int roll3 = Compute.d6();
            int lowRoll1 = 0;
            int lowRoll2 = 0;

            if ((roll1 <= roll2) && (roll1 <= roll3)) {
                lowRoll1 = roll1;
                lowRoll2 = Math.min(roll2, roll3);
            } else if ((roll2 <= roll1) && (roll2 <= roll3)) {
                lowRoll1 = roll2;
                lowRoll2 = Math.min(roll1, roll3);
            } else if ((roll3 <= roll1) && (roll3 <= roll2)) {
                lowRoll1 = roll3;
                lowRoll2 = Math.min(roll2, roll1);
            }
            nRoll = lowRoll1 + lowRoll2;
        }
        if (streak) {
            nRoll = 11;
        }
        nRoll += nMod;
        if (!advancedAMS) {
            nRoll = Math.min(Math.max(nRoll, 2), 12);
        } else {
            nRoll=Math.min(nRoll, 12);
        }
        if (nRoll<2) {
            return 0;
        }

        for (int[] element : clusterHitsTable) {
            if (element[0] == missiles) {
                return element[nRoll - 1];
            }
        }
        // BA missiles may have larger number of missiles than max entry on the
        // table
        // if so, take largest, subtract value and try again
        for (int i = clusterHitsTable.length - 1; i >= 0; i--) {
            if (missiles > clusterHitsTable[i][0]) {
                return clusterHitsTable[i][nRoll - 1] + Compute.missilesHit(missiles - clusterHitsTable[i][0], nMod, hotloaded, streak, advancedAMS);
            }
        }
        throw new RuntimeException("Could not find number of missiles in hit table");
    }

    /**
     * Returns the consciousness roll number
     *
     * @param hit -
     *            the <code>int</code> number of the crew hit currently being
     *            rolled.
     * @return The <code>int</code> number that must be rolled on 2d6 for the
     *         crew to stay conscious.
     */
    public static int getConsciousnessNumber(int hit) {
        switch (hit) {
        case 0:
            return 2;
        case 1:
            return 3;
        case 2:
            return 5;
        case 3:
            return 7;
        case 4:
            return 10;
        case 5:
            return 11;
        default:
            return Integer.MAX_VALUE;
        }
    }

    /**
     * This method checks to see if a line from a to b is affected by an ECM
     * field of the enemy of ae
     *
     * @param ae
     * @param a
     * @param b
     * @return
     */
    public static boolean isAffectedByECM(Entity ae, Coords a, Coords b) {
        return Compute.getECMFieldSize(ae, a, b) > 0;
    }

    /**
     * This method checks to see if a line from a to b is affected by an ECCM
     * field of the enemy of ae
     *
     * @param ae
     * @param a
     * @param b
     * @return
     */
    public static boolean isAffectedByECCM(Entity ae, Coords a, Coords b) {
        return Compute.getECCMFieldSize(ae, a, b) > 0;
    }

    /**
     * This method returns the highest number of enemy ECM fields of ae between points a and b
     *
     * @param ae
     * @param a
     * @param b
     * @return
     */
    public static double getECMFieldSize(Entity ae, Coords a, Coords b) {
        if(ae.getGame().getBoard().inSpace()) {
            //normal ECM effects don't apply in space
            return 0;
        }
        if ((a == null) || (b == null)) {
            return 0;
        }

        // Only grab enemies with active ECM
        Vector<Coords> vEnemyECMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyECMRanges = new Vector<Integer>(16);
        Vector<Double> vEnemyECMStrengths = new Vector<Double>(16);
        Vector<Coords> vFriendlyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECCMRanges = new Vector<Integer>(16);
        Vector<Double> vFriendlyECCMStrengths = new Vector<Double>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            if (ent.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null)) {
                vEnemyECMCoords.addElement(entPos);
                vEnemyECMRanges.addElement(new Integer(ent.getECMRange()));
                vEnemyECMStrengths.addElement(new Double(ent.getECMStrength()));
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                vFriendlyECCMCoords.addElement(entPos);
                vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                vFriendlyECCMStrengths.addElement(new Double(ent.getECCMStrength()));
            }

            // Check the ECM effects of the entity's passengers.
            for (Entity other : ent.getLoadedUnits()) {
                if (other.isEnemyOf(ae) && other.hasActiveECM() && (entPos != null)) {
                    vEnemyECMCoords.addElement(entPos);
                    vEnemyECMRanges.addElement(new Integer(other.getECMRange()));
                    vEnemyECMStrengths.addElement(new Double(other.getECMStrength()));
                }
                if (!other.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                    vFriendlyECCMCoords.addElement(entPos);
                    vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                    vFriendlyECCMStrengths.addElement(new Double(ent.getECCMStrength()));
                }
            }
        }

        // none? get out of here
        if (vEnemyECMCoords.size() == 0) {
            return 0;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, check each if they are ECM
        // affected
        double worstECM = 0;
        for (Coords c : coords) {
            // > 0: in friendly ECCM
            // 0: unaffected by enemy ECM
            // <0: affected by enemy ECM
            double ecmStatus = 0;
            // if we're at ae's Position, figure in a possible
            // iNarc ECM pod
            if (c.equals(ae.getPosition()) && ae.isINarcedWith(INarcPod.ECM)) {
                ecmStatus++;
            }
            // first, subtract 1 for each enemy ECM that affects us
            Enumeration<Integer> ranges = vEnemyECMRanges.elements();
            Enumeration<Double> strengths = vEnemyECMStrengths.elements();
            for (Coords enemyECMCoords : vEnemyECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    ecmStatus += strength;
                }
            }
            // now, add one for each friendly ECCM
            ranges = vFriendlyECCMRanges.elements();
            strengths = vFriendlyECCMStrengths.elements();
            for (Coords friendlyECCMCoords : vFriendlyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECCMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    ecmStatus -= strength;
                }
            }
            // if any coords in the line are affected, the whole line is
            if (ecmStatus > worstECM) {
                worstECM = ecmStatus;
            }
        }
        return worstECM;
    }

    /**
     * This method returns the highest number of enemy ECCM fields of ae between points a and b
     *
     * @param ae
     * @param a
     * @param b
     * @return
     */
    public static double getECCMFieldSize(Entity ae, Coords a, Coords b) {
        if(ae.getGame().getBoard().inSpace()) {
            //normal ECM effects don't apply in space
            return 0;
        }
        if ((a == null) || (b == null)) {
            return 0;
        }

        // Only grab enemies with active ECM
        Vector<Coords> vEnemyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyECCMRanges = new Vector<Integer>(16);
        Vector<Double> vEnemyECCMStrengths = new Vector<Double>(16);
        Vector<Coords> vFriendlyECMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECMRanges = new Vector<Integer>(16);
        Vector<Double> vFriendlyECMStrengths = new Vector<Double>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            if (ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                vEnemyECCMCoords.addElement(entPos);
                vEnemyECCMRanges.addElement(new Integer(ent.getECMRange()));
                vEnemyECCMStrengths.addElement(new Double(ent.getECMStrength()));
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null)) {
                vFriendlyECMCoords.addElement(entPos);
                vFriendlyECMRanges.addElement(new Integer(ent.getECMRange()));
                vFriendlyECMStrengths.addElement(new Double(ent.getECCMStrength()));
            }

            // Check the ECM effects of the entity's passengers.
            for (Entity other : ent.getLoadedUnits()) {
                if (other.isEnemyOf(ae) && other.hasActiveECCM() && (entPos != null)) {
                    vEnemyECCMCoords.addElement(entPos);
                    vEnemyECCMRanges.addElement(new Integer(other.getECMRange()));
                    vEnemyECCMStrengths.addElement(new Double(other.getECMStrength()));
                }
                if (!other.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null)) {
                    vFriendlyECMCoords.addElement(entPos);
                    vFriendlyECMRanges.addElement(new Integer(ent.getECMRange()));
                    vFriendlyECMStrengths.addElement(new Double(ent.getECCMStrength()));
                }
            }
        }

        // none? get out of here
        if (vEnemyECCMCoords.size() == 0) {
            return 0;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, check each if they are ECCM
        // affected
        double worstECCM = 0;
        for (Coords c : coords) {
            // > 0: in friendly ECM
            // 0: unaffected by enemy ECCM
            // <0: affected by enemy ECCM
            double eccmStatus = 0;
            // if we're at ae's Position, figure in a possible
            // iNarc ECM pod
            if (c.equals(ae.getPosition()) && ae.isINarcedWith(INarcPod.ECM)) {
                eccmStatus--;
            }
            // first, subtract 1 for each enemy ECCM that affects us
            Enumeration<Integer> ranges = vEnemyECCMRanges.elements();
            Enumeration<Double> strengths = vEnemyECCMStrengths.elements();
            for (Coords enemyECCMCoords : vEnemyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECCMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    eccmStatus += strength;
                }
            }
            // now, add one for each friendly ECCM
            ranges = vFriendlyECMRanges.elements();
            strengths = vFriendlyECMStrengths.elements();
            for (Coords friendlyECMCoords : vFriendlyECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    eccmStatus -= strength;
                }
            }
            // if any coords in the line are affected, the whole line is
            if (eccmStatus > worstECCM) {
                worstECCM = eccmStatus;
            }
        }
        return worstECCM;
    }

    /**
     *  This method checks to see if a line from a to b is affected by an Angel
     *  ECM field of the enemy of ae
     *
     * @param ae
     * @param a
     * @param b
     * @return count that shows if you are in an friendly ECCM field positive
     *         number means you are in an friendly ECCM field Negative number
     *         means you are in a enemy ECM field 0 means you are not effect by
     *         enemy or friendly fields.
     */
    public static boolean isAffectedByAngelECM(Entity ae, Coords a, Coords b) {
        return Compute.getAngelECMFieldSize(ae, a, b) > 0;
    }

    public static double getAngelECMFieldSize(Entity ae, Coords a, Coords b) {
        if(ae.getGame().getBoard().inSpace()) {
            //normal Angel ECM effects don't apply in space
            return 0;
        }
        if ((a == null) || (b == null)) {
            return 0;
        }

        // Only grab enemies with active angel ECM
        Vector<Coords> vEnemyAngelECMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyAngelECMRanges = new Vector<Integer>(16);
        Vector<Double> vEnemyAngelECMStrengths = new Vector<Double>(16);
        Vector<Coords> vFriendlyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECCMRanges = new Vector<Integer>(16);
        Vector<Double> vFriendlyECCMStrengths = new Vector<Double>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            // add each angel ECM at its ECM strength
            if (ent.isEnemyOf(ae) && ent.hasActiveAngelECM() && (entPos != null)) {
                vEnemyAngelECMCoords.addElement(entPos);
                vEnemyAngelECMRanges.addElement(new Integer(ent.getECMRange()));
                vEnemyAngelECMStrengths.add(ent.getECMStrength());
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                vFriendlyECCMCoords.addElement(entPos);
                vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                vFriendlyECCMStrengths.add(ent.getECCMStrength());
            }

            // Check the angel ECM effects of the entity's passengers.
            for (Entity other : ent.getLoadedUnits()) {
                if (other.isEnemyOf(ae) && other.hasActiveAngelECM() && (entPos != null)) {
                    vEnemyAngelECMCoords.addElement(entPos);
                    vEnemyAngelECMRanges.addElement(new Integer(other.getECMRange()));
                    vEnemyAngelECMStrengths.add(ent.getECMStrength());
                }
                if (!other.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                    vFriendlyECCMCoords.addElement(entPos);
                    vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                    vFriendlyECCMStrengths.add(ent.getECMStrength());
                }
            }
        }

        // none? get out of here
        if (vEnemyAngelECMCoords.size() == 0) {
            return 0;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, check each if they are ECM
        // affected
        double worstECM = 0;
        for (Coords c : coords) {
            // > 0: in friendly ECCM
            // 0: unaffected by enemy ECM
            // <0: affected by enemy angel ECM
            double ecmStatus = 0;
            // first, subtract 2 for each enemy angel ECM that affects us
            Enumeration<Integer> ranges = vEnemyAngelECMRanges.elements();
            Enumeration<Double> strengths = vEnemyAngelECMStrengths.elements();
            for (Coords enemyECMCoords : vEnemyAngelECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    ecmStatus += strength;
                }
            }
            // now, add one for each friendly ECCM
            ranges = vFriendlyECCMRanges.elements();
            strengths = vFriendlyECCMStrengths.elements();
            for (Coords friendlyECCMCoords : vFriendlyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECCMCoords);
                double strength = strengths.nextElement().doubleValue();

                if (nDist <= range) {
                    ecmStatus -= strength;
                }
            }
            // if any coords in the line are affected, the whole line is
            if (ecmStatus > worstECM) {
                worstECM = ecmStatus;
            }
        }
        return worstECM;
    }

    /**
     * Check for ECM bubbles in Ghost Target mode along the path from a to b and return the highest
     * target roll. -1 if no Ghost Targets
     */
    public static int getGhostTargetNumber(Entity ae, Coords a, Coords b) {
        if(ae.getGame().getBoard().inSpace()) {
            //ghost targets don't work in space
            return 0;
        }
        if ((a == null) || (b == null)) {
            return 0;
        }

        // Only grab enemies with active ECM
        //need to create two hashtables for ghost targeting, one with mods
        //and one with booleans indicating that this ghost target was intersected
        //the keys will be the entity id
        Hashtable<Integer, Boolean> hEnemyGTCrossed = new Hashtable<Integer, Boolean>();
        Hashtable<Integer, Integer> hEnemyGTMods = new Hashtable<Integer, Integer>();
        Vector<Coords> vEnemyECMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyECMRanges = new Vector<Integer>(16);
        Vector<Double> vEnemyECMStrengths = new Vector<Double>(16);
        Vector<Coords> vEnemyGTCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyGTRanges = new Vector<Integer>(16);
        Vector<Integer> vEnemyGTId = new Vector<Integer>(16);
        Vector<Coords> vFriendlyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECCMRanges = new Vector<Integer>(16);
        Vector<Double> vFriendlyECCMStrengths = new Vector<Double>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            if (ent.isEnemyOf(ae) && ent.hasGhostTargets(true) && (entPos != null)) {
                vEnemyGTCoords.addElement(entPos);
                vEnemyGTRanges.addElement(new Integer(ent.getECMRange()));
                vEnemyGTId.addElement(new Integer(ent.getId()));
                hEnemyGTCrossed.put(ent.getId(), false);
                hEnemyGTMods.put(ent.getId(), ent.getGhostTargetRollMoS());
            }
            if (ent.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null)) {
                vEnemyECMCoords.addElement(entPos);
                vEnemyECMRanges.addElement(new Integer(ent.getECMRange()));
                vEnemyECMStrengths.add(ent.getECMStrength());
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                vFriendlyECCMCoords.addElement(entPos);
                vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                vFriendlyECCMStrengths.add(ent.getECCMStrength());
            }

            // Check the ECM effects of the entity's passengers.
            for (Entity other : ent.getLoadedUnits()) {
                if (other.isEnemyOf(ae) && other.hasGhostTargets(true) && (entPos != null)) {
                    vEnemyGTCoords.addElement(entPos);
                    vEnemyGTRanges.addElement(new Integer(other.getECMRange()));
                    vEnemyGTId.addElement(new Integer(ent.getId()));
                    hEnemyGTCrossed.put(ent.getId(), false);
                    hEnemyGTMods.put(ent.getId(), ent.getGhostTargetRollMoS());
                }
                if (other.isEnemyOf(ae) && other.hasActiveECM() && (entPos != null)) {
                    vEnemyECMCoords.addElement(entPos);
                    vEnemyECMRanges.addElement(new Integer(other.getECMRange()));
                    vEnemyECMStrengths.add(ent.getECMStrength());
                }
                if (!other.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null)) {
                    vFriendlyECCMCoords.addElement(entPos);
                    vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
                    vFriendlyECCMStrengths.add(ent.getECCMStrength());
                }
            }
        }

        // none? get out of here
        if (vEnemyGTCoords.size() == 0) {
            return -1;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, if they are not eccm'ed by the enemy then add any Ghost Targets
        //to the hashlist
        for (Coords c : coords) {
            // < 0: in friendly ECCM
            // 0: unaffected by enemy ECM
            // >0: affected by enemy ECM
            int ecmStatus = 0;
            // first, subtract 1 for each enemy ECM that affects us
            Enumeration<Integer> ranges = vEnemyECMRanges.elements();
            Enumeration<Double> strengths = vEnemyECMStrengths.elements();
            for (Coords enemyECMCoords : vEnemyECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    ecmStatus += strength;
                }
            }
            // now, add one for each friendly ECCM
            ranges = vFriendlyECCMRanges.elements();
            strengths = vFriendlyECCMStrengths.elements();
            for (Coords friendlyECCMCoords : vFriendlyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECCMCoords);
                double strength = strengths.nextElement().doubleValue();
                if (nDist <= range) {
                    ecmStatus -= strength;
                }
            }

            if(ecmStatus >= 0) {
                //find any new Ghost Targets that we have crossed
                ranges = vEnemyGTRanges.elements();
                Enumeration<Integer> ids = vEnemyGTId.elements();
                for (Coords enemyGTCoords : vEnemyGTCoords) {
                    int range = ranges.nextElement().intValue();
                    int id = ids.nextElement().intValue();
                    int nDist = c.distance(enemyGTCoords);
                    if ((nDist <= range) && !hEnemyGTCrossed.get(id)) {
                        hEnemyGTCrossed.put(id, true);
                    }
                }
            }
        }

        //ok so now we have a hashtable that tells us which Ghost Targets have been crossed
        //lets loop through that and identify the highest bonus and count the total number crossed
        int totalGT = 0;
        int highestMod = -1;
        Enumeration<Integer> ids = hEnemyGTCrossed.keys();
        while(ids.hasMoreElements()) {
            int id = ids.nextElement();
            if(hEnemyGTCrossed.get(id)) {
                if(hEnemyGTMods.get(id) > highestMod) {
                    highestMod = hEnemyGTMods.get(id);
                } else {
                    totalGT++;
                }
            }
        }
        return highestMod + totalGT;
    }

    /**
     * Check for the total number of fighter/small craft ECM bubbles in space along the path from a to b
     */
    public static int getSmallCraftECM(Entity ae, Coords a, Coords b) {
        if(!ae.getGame().getBoard().inSpace()) {
            //only matters in space
            return 0;
        }
        //Only grab enemies with active ECM
        Vector<Coords> vEnemyECMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyECMRanges = new Vector<Integer>(16);
        Vector<Coords> vFriendlyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECCMRanges = new Vector<Integer>(16);
        Vector<Coords> vFriendlyBAPCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyBAPRanges = new Vector<Integer>(16);
        Vector<Integer> vFriendlyBAPFacings = new Vector<Integer>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            if((entPos == null) && (ent.getTransportId() != Entity.NONE)) {
                Entity carrier = ae.game.getEntity(ent.getTransportId());
                if((null != carrier) && carrier.loadedUnitsHaveActiveECM()) {
                    entPos = carrier.getPosition();
                }
            }
            if (ent.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null) && !ent.isLargeCraft()) {
                vEnemyECMCoords.addElement(entPos);
                vEnemyECMRanges.addElement(new Integer(ent.getECMRange()));
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null) && !ent.isLargeCraft()) {
                vFriendlyECCMCoords.addElement(entPos);
                vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
            }
            if(!ent.isEnemyOf(ae) && ent.hasBAP(false) && (entPos != null)) {
                vFriendlyBAPCoords.addElement(entPos);
                vFriendlyBAPRanges.addElement(new Integer(ent.getBAPRange()));
                vFriendlyBAPFacings.addElement(new Integer(ent.getFacing()));
            }

            //TODO: do docked dropships give ECM benefit?
        }

        // none? get out of here
        if (vEnemyECMCoords.size() == 0) {
            return 0;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, check each if they are ECM
        // affected
        int totalECM = 0;
        //check for split hexes
        boolean bDivided = (a.degree(b) % 60 == 30);
        int x = 0;
        int prevEcmStatus = 0;
        boolean prevEccmPresent = false;
        for (Coords c : coords) {
            int ecmStatus = 0;
            boolean eccmPresent = false;
            // first, subtract 1 for each enemy ECM that affects us
            Enumeration<Integer> ranges = vEnemyECMRanges.elements();
            for (Coords enemyECMCoords : vEnemyECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECMCoords);
                if (nDist <= range) {
                    ecmStatus++;
                }
            }
            //now check for friendly eccm
            ranges = vFriendlyECCMRanges.elements();
            for (Coords friendlyECCMCoords : vFriendlyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECCMCoords);
                if (nDist <= range) {
                    eccmPresent = true;
                    break;
                }
            }
            //if eccm still not present, check for BAP
            if(!eccmPresent) {
                ranges = vFriendlyBAPRanges.elements();
                Enumeration<Integer> facings = vFriendlyBAPFacings.elements();
                for (Coords friendlyBAPCoords : vFriendlyBAPCoords) {
                    int range = ranges.nextElement().intValue();
                    int nDist = c.distance(friendlyBAPCoords);
                    int facing = facings.nextElement().intValue();
                    if (nDist <= range) {
                        //still might need to check for right arc if using medium range
                        if((range < 7) || Compute.isInArc(friendlyBAPCoords, facing, c, ARC_NOSE)) {
                            eccmPresent = true;
                            break;
                        }
                    }
                }
            }
            // if any coords in the line are affected, the whole line is
            if(!bDivided || (x % 3 == 0)) {
                if ((ecmStatus > 0) && !eccmPresent) {
                    totalECM++;
                }
            } else if ((x % 3 == 2)) {
                //if we are looking at the second split hex then both this one and the prior need to have ECM
                //becaue the advantage should go to the defender
                if ((ecmStatus > 0) && !eccmPresent && (prevEcmStatus > 0) && !prevEccmPresent) {
                    totalECM++;
                }
            }
            x++;
            prevEccmPresent = eccmPresent;
            prevEcmStatus = ecmStatus;



        }
        return totalECM;
    }

    /**
     * Check for the total number of fighter/small craft ECM bubbles in space along the path from a to b
     */
    public static int getLargeCraftECM(Entity ae, Coords a, Coords b) {
        if(!ae.getGame().getBoard().inSpace()) {
            //only matters in space
            return 0;
        }
        //Only grab enemies with active ECM
        Vector<Coords> vEnemyECMCoords = new Vector<Coords>(16);
        Vector<Integer> vEnemyECMRanges = new Vector<Integer>(16);
        Vector<Coords> vFriendlyECCMCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyECCMRanges = new Vector<Integer>(16);
        Vector<Coords> vFriendlyBAPCoords = new Vector<Coords>(16);
        Vector<Integer> vFriendlyBAPRanges = new Vector<Integer>(16);
        Vector<Integer> vFriendlyBAPFacings = new Vector<Integer>(16);
        for (Enumeration<Entity> e = ae.game.getEntities(); e.hasMoreElements();) {
            Entity ent = e.nextElement();
            Coords entPos = ent.getPosition();
            if((entPos == null) && (ent.getTransportId() != Entity.NONE)) {
                Entity carrier = ae.game.getEntity(ent.getTransportId());
                if((null != carrier) && carrier.loadedUnitsHaveActiveECM()) {
                    entPos = carrier.getPosition();
                }
            }
            if (ent.isEnemyOf(ae) && ent.hasActiveECM() && (entPos != null) && ent.isLargeCraft()) {
                vEnemyECMCoords.addElement(entPos);
                vEnemyECMRanges.addElement(new Integer(ent.getECMRange()));
            }
            if (!ent.isEnemyOf(ae) && ent.hasActiveECCM() && (entPos != null) && !ent.isLargeCraft()) {
                vFriendlyECCMCoords.addElement(entPos);
                vFriendlyECCMRanges.addElement(new Integer(ent.getECMRange()));
            }
            if(!ent.isEnemyOf(ae) && ent.hasBAP(false) && (entPos != null)) {
                vFriendlyBAPCoords.addElement(entPos);
                vFriendlyBAPRanges.addElement(new Integer(ent.getBAPRange()));
                vFriendlyBAPFacings.addElement(new Integer(ent.getFacing()));

            }
            //TODO: do docked dropships give ECM benefit?
        }

        // none? get out of here
        if (vEnemyECMCoords.size() == 0) {
            return 0;
        }

        // get intervening Coords.
        ArrayList<Coords> coords = Coords.intervening(a, b);
        // loop through all intervening coords, check each if they are ECM
        // affected
        int totalECM = 0;
        boolean bDivided = (a.degree(b) % 60 == 30);
        int x = 0;
        int prevEcmStatus = 0;
        for (Coords c : coords) {
            // > 0: in friendly ECCM
            // 0: unaffected by enemy ECM
            // <0: affected by enemy ECM
            int ecmStatus = 0;
            // first, subtract 1 for each enemy ECM that affects us
            Enumeration<Integer> ranges = vEnemyECMRanges.elements();
            for (Coords enemyECMCoords : vEnemyECMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(enemyECMCoords);
                if (nDist <= range) {
                    ecmStatus++;
                }
            }
            //now check for friendly small craft eccm
            ranges = vFriendlyECCMRanges.elements();
            for (Coords friendlyECCMCoords : vFriendlyECCMCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyECCMCoords);
                if (nDist <= range) {
                    ecmStatus--;
                }
            }
            //now check BAP
            ranges = vFriendlyBAPRanges.elements();
            Enumeration<Integer> facings = vFriendlyBAPFacings.elements();
            for (Coords friendlyBAPCoords : vFriendlyBAPCoords) {
                int range = ranges.nextElement().intValue();
                int nDist = c.distance(friendlyBAPCoords);
                int facing = facings.nextElement().intValue();
                if (nDist <= range) {
                    //still might need to check for right arc if using medium range
                    if((range < 7) || Compute.isInArc(friendlyBAPCoords, facing, c, ARC_NOSE)) {
                        ecmStatus = ecmStatus - 2;
                    }
                }
            }
            // if any coords in the line are affected, the whole line is
            if(!bDivided || (x % 3 == 0)) {
                if (ecmStatus > 0) {
                    totalECM++;
                }
            } else if(x % 3 == 2) {
                //if we are looking at the second split hex then both this one and the prior need to have ECM
                //becaue the advantage should go to the defender
                if ((ecmStatus > 0) && (prevEcmStatus > 0)) {
                    totalECM++;
                }
            }
            x++;
            prevEcmStatus = ecmStatus;
        }
        return totalECM;
    }

    /**
     * Get the base to-hit number of a space bomb attack by the given attacker upon the
     * given defender
     *
     * @param attacker -
     *            the <code>Entity</code> conducting the leg attack.
     * @param defender -
     *            the <code>Entity</code> being attacked.
     * @return The base <code>ToHitData</code> of the attack.
     */
    public static ToHitData getSpaceBombBaseToHit(Entity attacker,
            Entity defender, IGame game) {
        int base = TargetRoll.IMPOSSIBLE;
        StringBuffer reason = new StringBuffer();

        if(!(attacker instanceof Aero)) {
            return new ToHitData(base, "attacker is not an Aero");
        }

        Aero a = (Aero)attacker;

        //the fighters nose must be aligned with its direction of travel
        boolean rightFacing = false;
        //using normal movement, I think this means that the last move can't be a turn
        if(!game.useVectorMove()) {
            rightFacing = true;
        }
        //for advanced movement, it must be aligned with largest vector
        if(game.useVectorMove()) {
            for(int h:attacker.getHeading()) {
                if(h == attacker.facing) {
                    rightFacing = true;
                    break;
                }
            }
        }

        boolean canTarget = false;
        Coords attackCoords = null;
        for(Coords c:attacker.getPassedThrough()) {
            for (Enumeration<Entity> e = game.getEntities(c); e.hasMoreElements(); ) {
                Entity target = e.nextElement();
                if(target.getId() == defender.getId()) {
                    canTarget = true;
                }
            }
            if(canTarget) {
                break;
            } else {
                attackCoords = c;
            }
        }
        if(null == attackCoords) {
            attackCoords = attacker.getPosition();
        }

        //must be in control
        if(a.isOutControlTotal()) {
            reason.append("the attacker is out of control");
        }
        else if(a.getSpaceBombs().size() < 1) {
            reason.append("the attacker has no useable bombs");
        } else if(!rightFacing) {
            reason.append("the attacker is not facing the direction of travel");
        }
        //attacker and defender must both be in space hex
        else if(!game.getBoard().getHex(attacker.getPosition()).containsTerrain(Terrains.SPACE)) {
            reason.append("attacker not in space hex");
        } else if(!game.getBoard().getHex(defender.getPosition()).containsTerrain(Terrains.SPACE)) {
            reason.append("defender not in space hex");
        } else if(!canTarget) {
            reason.append("defender is not in hex passed through by attacker this turn");
        }
        //the defender must weight 10000+ tons
        else if(defender.weight < 10000) {
            reason.append("the defender weighs less than 10,000 tons");
        }

        //ok if we are still alive then lets calculate the tohit
        else {
            base = attacker.getCrew().getGunnery();
            reason.append("base");
        }

        ToHitData toHit = new ToHitData(base, reason.toString(), ToHitData.HIT_NORMAL,
                    defender.sideTable(attackCoords));

        toHit.addModifier(+4,"space bomb attack");
        if(attacker.mpUsed > 0) {
            toHit.addModifier(attacker.mpUsed, "attacker thrust");
        }
        if(defender.mpUsed > 0) {
            toHit.addModifier(defender.mpUsed, "defender thrust");
        }
        if((defender instanceof SpaceStation) || (defender.getWalkMP() == 0)) {
            toHit.addModifier(-4, "immobile");
        }
        if(defender.weight < 100000) {
            int penalty = (int)Math.ceil((100000 - defender.weight)/10000);
            toHit.addModifier(penalty, "defender weight");
        }

        return toHit;
    }


    /**
     * Get the base to-hit number of a Leg Attack by the given attacker upon the
     * given defender
     *
     * @param attacker -
     *            the <code>Entity</code> conducting the leg attack.
     * @param defender -
     *            the <code>Entity</code> being attacked.
     * @return The base <code>ToHitData</code> of the attack.
     */
    public static ToHitData getLegAttackBaseToHit(Entity attacker, Entity defender) {
        int men = 0;
        int base = TargetRoll.IMPOSSIBLE;
        StringBuffer reason = new StringBuffer();

        // Can only attack a Mek's legs.
        if (!(defender instanceof Mech)) {
            reason.append("Defender is not a Mek.");
        }

        // Can't attack if flying
        else if (attacker.getElevation()>defender.getElevation()) {
            reason.append("Cannot do leg attack while flying.");
        }
        // Can't target a transported entity.
        else if (Entity.NONE != defender.getTransportId()) {
            reason.append("Target is a passenger.");
        }

        // Can't target a entity conducting a swarm attack.
        else if (Entity.NONE != defender.getSwarmTargetId()) {
            reason.append("Target is swarming a Mek.");
        }

        // Attacker can't be swarming.
        else if (Entity.NONE != attacker.getSwarmTargetId()) {
            reason.append("Attacker is swarming.");
        }

        // Handle BattleArmor attackers.
        else if (attacker instanceof BattleArmor) {
            BattleArmor inf = (BattleArmor) attacker;

            // Battle Armor units can't Leg Attack if they're burdened.
            if (inf.isBurdened()) {
                reason.append("Launcher not jettisoned.");
            } else {
                men = inf.getShootingStrength();
                if (men >= 4) {
                    base = inf.getCrew().getPiloting();
                } else if (men >= 3) {
                    base = inf.getCrew().getPiloting() + 2;
                } else if (men >= 2) {
                    base = inf.getCrew().getPiloting() + 5;
                } else if (men >= 1) {
                    base = inf.getCrew().getPiloting() + 7;
                }
                reason.append(men);
                reason.append(" trooper(s) active");
            }
        } else if (attacker instanceof Infantry) {
            // Non-BattleArmor infantry need many more men.
            Infantry inf = (Infantry) attacker;
            men = inf.getShootingStrength();
            if (men >= 22) {
                base = inf.getCrew().getPiloting();
            } else if (men >= 16) {
                base = inf.getCrew().getPiloting() + 2;
            } else if (men >= 10) {
                base = inf.getCrew().getPiloting() + 5;
            } else if (men >= 5) {
                base = inf.getCrew().getPiloting() + 7;
            }
            reason.append(men);
            reason.append(" men alive");
        } else {
            // No one else can conduct leg attacks.
            reason.append("Attacker is not infantry.");
        }
        ToHitData toReturn = new ToHitData(base, reason.toString(), ToHitData.HIT_KICK, ToHitData.SIDE_FRONT);
        if ((defender instanceof Mech) && ((Mech)defender).isIndustrial()) {
           toReturn.addModifier(-1, "targeting industrial mech");
        }
        // Return the ToHitData for this attack.
        // N.B. we attack the legs.
        return toReturn;
    }

    /**
     * Get the base to-hit number of a Swarm Mek by the given attacker upon the
     * given defender.
     *
     * @param attacker -
     *            the <code>Entity</code> swarming.
     * @param defender -
     *            the <code>Entity</code> being swarmed.
     * @return The base <code>ToHitData</code> of the mek.
     */
    public static ToHitData getSwarmMekBaseToHit(Entity attacker, Entity defender) {
        int men = 0;
        int base = TargetRoll.IMPOSSIBLE;
        StringBuffer reason = new StringBuffer();

        // Can only swarm a Mek.
        if (!(defender instanceof Mech) && !(defender instanceof Tank)) {
            reason.append("Defender is not a Mek or vehicle.");
        }

        // Can't target a transported entity.
        else if (Entity.NONE != defender.getTransportId()) {
            reason.append("Target is a passenger.");
        }

        // Attacker can't be swarming.
        else if (Entity.NONE != attacker.getSwarmTargetId()) {
            reason.append("Attacker is swarming.");
        }

        // Can't target a entity invloved in a swarm attack.
        else if (Entity.NONE != defender.getSwarmAttackerId()) {
            reason.append("Target is already being swarmed.");
        }

        // Can't target a entity conducting a swarm attack.
        else if (Entity.NONE != defender.getSwarmTargetId()) {
            reason.append("Target is swarming a Mek.");
        }

        // Can't swarm a friendly Mek.
        // See http://www.classicbattletech.com/w3t/showflat.php?Cat=&Board=ask&Number=632321&page=0&view=collapsed&sb=5&o=0&fpart=
        else if (!attacker.isEnemyOf(defender) && !attacker.getGame().getOptions().booleanOption("friendly_fire")) {
            reason.append("Can only swarm an enemy.");
        }

        // Handle BattleArmor attackers.
        else if (attacker instanceof BattleArmor) {
            BattleArmor inf = (BattleArmor) attacker;

            // Battle Armor units can't Leg Attack if they're burdened.
            if (inf.isBurdened()) {
                reason.append("Launcher not jettisoned.");
            } else {
                men = inf.getShootingStrength();
                if (men >= 4) {
                    base = inf.getCrew().getPiloting() + 2;
                } else if (men >= 1) {
                    base = inf.getCrew().getPiloting() + 5;
                }
                reason.append(men);
                reason.append(" trooper(s) active");
            }
        }

        // Non-BattleArmor infantry need many more men.
        else if (attacker instanceof Infantry) {
            Infantry inf = (Infantry) attacker;
            men = inf.getShootingStrength();
            if (men >= 22) {
                base = inf.getCrew().getPiloting() + 2;
            } else if (men >= 16) {
                base = inf.getCrew().getPiloting() + 5;
            }
            reason.append(men);
            reason.append(" men alive");
        }

        // No one else can conduct leg attacks.
        else {
            reason.append("Attacker is not infantry.");
        }

        ToHitData toReturn = new ToHitData(base, reason.toString());
        if ((defender instanceof Mech) && ((Mech)defender).isIndustrial()) {
            toReturn.addModifier(-1, "targeting industrial mech");
        }
        // Return the ToHitData for this attack.
        return toReturn;
    }

    public static boolean canPhysicalTarget(IGame game, int entityId, Targetable target) {

        if (PunchAttackAction.toHit(game, entityId, target, PunchAttackAction.LEFT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (PunchAttackAction.toHit(game, entityId, target, PunchAttackAction.RIGHT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (KickAttackAction.toHit(game, entityId, target, KickAttackAction.LEFT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (KickAttackAction.toHit(game, entityId, target, KickAttackAction.RIGHT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if ((game.getEntity(entityId) instanceof QuadMech) && ((KickAttackAction.toHit(game, entityId, target, KickAttackAction.LEFTMULE).getValue() != TargetRoll.IMPOSSIBLE) || (KickAttackAction.toHit(game, entityId, target, KickAttackAction.RIGHTMULE).getValue() != TargetRoll.IMPOSSIBLE))) {
            return true;
        }

        if (BrushOffAttackAction.toHit(game, entityId, target, BrushOffAttackAction.LEFT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (BrushOffAttackAction.toHit(game, entityId, target, BrushOffAttackAction.RIGHT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (new ThrashAttackAction(entityId, target).toHit(game).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (ProtomechPhysicalAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (PushAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (LayExplosivesAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (TripAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (GrappleAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (BreakGrappleAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        for (Mounted club : game.getEntity(entityId).getClubs()) {
            if (null != club) {
                if (ClubAttackAction.toHit(game, entityId, target, club, ToHitData.HIT_NORMAL).getValue() != TargetRoll.IMPOSSIBLE) {
                    return true;
                }
            }
        }

        if (JumpJetAttackAction.toHit(game, entityId, target, JumpJetAttackAction.BOTH).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }
        if (JumpJetAttackAction.toHit(game, entityId, target, JumpJetAttackAction.LEFT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }
        if (JumpJetAttackAction.toHit(game, entityId, target, JumpJetAttackAction.RIGHT).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        if (BAVibroClawAttackAction.toHit(game, entityId, target).getValue() != TargetRoll.IMPOSSIBLE) {
            return true;
        }

        return false;
    }

    /**
     * Can movement between the two coordinates be on pavement (which includes
     * roads and bridges)? If so it will override prohibited terrain, it may
     * change movement costs, and it may lead to skids.
     *
     * @param game -
     *            the <code>IGame</code> object.
     * @param src -
     *            the <code>Coords</code> being left.
     * @param dest -
     *            the <code>Coords</code> being entered.
     * @param entity -
     *            the <code>Entity</code> that is moving
     * @return <code>true</code> if movement between <code>src</code> and
     *         <code>dest</code> can be on pavement; <code>false</code>
     *         otherwise.
     */
    public static boolean canMoveOnPavement(IGame game, Coords src, Coords dest, MovePath movePath) {
        final IHex srcHex = game.getBoard().getHex(src);
        final IHex destHex = game.getBoard().getHex(dest);
        final int src2destDir = src.direction(dest);
        final int dest2srcDir = (src2destDir + 3) % 6;
        boolean result = false;

        // We may be moving in the same hex.
        if (src.equals(dest) && (srcHex.containsTerrain(Terrains.PAVEMENT) || srcHex.containsTerrain(Terrains.ROAD) || srcHex.containsTerrain(Terrains.BRIDGE))) {
            result = true;
        }

        // If the source is a pavement hex, then see if the destination
        // hex is also a pavement hex or has a road or bridge that exits
        // into the source hex and the entity is climbing onto the bridge.
        else if (srcHex.containsTerrain(Terrains.PAVEMENT)
                && (destHex.containsTerrain(Terrains.PAVEMENT)
                   || destHex.containsTerrainExit(Terrains.ROAD,dest2srcDir)
                   || (destHex.containsTerrainExit(Terrains.BRIDGE, dest2srcDir)
                      && movePath.getFinalClimbMode()))) {
            result = true;
        }

        // See if the source hex has a road or bridge (and the entity is on the
        // bridge) that exits into the destination hex, and the dest hex has
        // pavement or a corresponding exit to the src hex
        else if ((srcHex.containsTerrainExit(Terrains.ROAD, src2destDir)
                 || (srcHex.containsTerrainExit(Terrains.BRIDGE, src2destDir)
                    && (movePath.getLastStep().getElevation() == srcHex.terrainLevel(Terrains.BRIDGE_ELEV))))
                && (destHex.containsTerrainExit(Terrains.ROAD, dest2srcDir)
                   || (destHex.containsTerrainExit(Terrains.BRIDGE, dest2srcDir)
                      && movePath.getFinalClimbMode())
                   || destHex.containsTerrain(Terrains.PAVEMENT))) {
            result = true;
        }

        return result;
    }

    /**
     * Determines whether the attacker and the target are in the same building.
     *
     * @return true if the target can and does occupy the same building, false
     *         otherwise.
     */
    public static boolean isInSameBuilding(IGame game, Entity attacker, Targetable target) {
        if (!(target instanceof Entity)) {
            return false;
        }
        Entity targetEntity = (Entity) target;
        if (!Compute.isInBuilding(game, attacker) || !Compute.isInBuilding(game, targetEntity)) {
            return false;
        }

        Building attkBldg = game.getBoard().getBuildingAt(attacker.getPosition());
        Building targBldg = game.getBoard().getBuildingAt(target.getPosition());

        return attkBldg.equals(targBldg);
    }

    /**
     * Determine if the given unit is inside of a building at the given
     * coordinates.
     *
     * @param game -
     *            the <code>IGame</code> object. This value may be
     *            <code>null</code>.
     * @param entity -
     *            the <code>Entity</code> to be checked. This value may be
     *            <code>null</code>.
     * @return <code>true</code> if the entity is inside of the building at
     *         those coordinates. <code>false</code> if there is no building
     *         at those coordinates or if the entity is on the roof or in the
     *         air above the building, or if any input argument is
     *         <code>null</code>.
     */
    public static boolean isInBuilding(IGame game, Entity entity) {

        // No game, no building.
        if (game == null) {
            return false;
        }

        // Null entities can't be in a building.
        if (entity == null) {
            return false;
        }

        // Call the version of the function that requires coordinates.
        return Compute.isInBuilding(game, entity, entity.getPosition());
    }

    /**
     * Determine if the given unit is inside of a building at the given
     * coordinates.
     *
     * @param game -
     *            the <code>IGame</code> object. This value may be
     *            <code>null</code>.
     * @param entity -
     *            the <code>Entity</code> to be checked. This value may be
     *            <code>null</code>.
     * @param coords -
     *            the <code>Coords</code> of the building hex. This value may
     *            be <code>null</code>.
     * @return <code>true</code> if the entity is inside of the building at
     *         those coordinates. <code>false</code> if there is no building
     *         at those coordinates or if the entity is on the roof or in the
     *         air above the building, or if any input argument is
     *         <code>null</code>.
     */
    public static boolean isInBuilding(IGame game, Entity entity, Coords coords) {

        // No game, no building.
        if (game == null) {
            return false;
        }

        // Null entities can't be in a building.
        if (entity == null) {
            return false;
        }

        // Null coordinates can't have buildings.
        if (coords == null) {
            return false;
        }

        // Get the Hex at those coordinates.

        return Compute.isInBuilding(game, entity.getElevation(), coords);
    }

    static boolean isInBuilding(IGame game, int entityElev, Coords coords) {

        // Get the Hex at those coordinates.
        final IHex curHex = game.getBoard().getHex(coords);

        if (curHex == null) {
            // probably off board artillery or reinforcement
            return false;
        }

        // The entity can't be inside of a building that isn't there.
        if (!curHex.containsTerrain(Terrains.BLDG_ELEV)) {
            return false;
        }

        // Get the elevations occupied by the building.
        int bldgHeight = curHex.terrainLevel(Terrains.BLDG_ELEV);
        int basement = 0;
        if (curHex.containsTerrain(Terrains.BLDG_BASEMENT)) {
            basement = curHex.terrainLevel(Terrains.BLDG_BASEMENT);
        }

        // Return true if the entity is in the range of building elevations.
        if ((entityElev >= (-basement)) && (entityElev < (bldgHeight))) {
            return true;
        }

        // Entity is not *inside* of the building.
        return false;
    }

    /**
     * scatter from hex according to dive bombing rules
     * (1d6 of scatter distance)
     * @param coords The <code>Coords</code> to scatter from
     * @return the <code>Coords</code> scattered to
     */
    public static Coords scatterDiveBombs(Coords coords) {
        return Compute.scatter(coords, Compute.d6());
    }

    /**
     * scatter from hex according to direct fire artillery rules
     * (1d6 of scatter distance)
     * @param coords The <code>Coords</code> to scatter from
     * @return the <code>Coords</code> scattered to
     */
    public static Coords scatterDirectArty(Coords coords) {
        return Compute.scatter(coords, Compute.d6());
    }

    /**
     * scatter from a hex according, roll d6 to choose scatter direction
     * @param coords The <code>Coords</code> to scatter from
     * @param margin the <code>int</code> margin of failure,
     * scatter distance will be the margin of failure
     * @return the <code>Coords</code> scattered to
     */
    public static Coords scatter(Coords coords, int margin) {
        int scatterDirection = Compute.d6(1) - 1;
        return coords.translated(scatterDirection, margin);
    }

    /**
     * scatter from hex according to atmospheric drop rules
     * d6 for direction, 1d6 per point of MOF
     * @param coords The <code>Coords</code> to scatter from
     * @param margin the <code>int</code> margin of failure
     * @return the <code>Coords</code> scattered to
     */
    public static Coords scatterAssaultDrop(Coords coords, int margin) {
        int scatterDirection = Compute.d6(1) - 1;
        int distance = Compute.d6(margin);
        return coords.translated(scatterDirection, distance);
    }

    /**
     * Gets a ring of hexes at a specified distance from the centre
     *
     * @param centre
     *            The centre point of the ring
     * @param range
     *            The radius of the ring
     */
    public static ArrayList<Coords> coordsAtRange(Coords centre, int range) {
        ArrayList<Coords> result = new ArrayList<Coords>(range * 6);
        if (range < 1) {
            result.add(centre);
            return result;
        }
        for (int dir = 0; dir < 6; dir++) {
            Coords corner = centre.translated(dir, range);
            for (int count = 0; count < range; count++) {
                result.add(corner);
                corner = corner.translated((dir + 2) % 6);
            }
        }
        return result;
    }

    /**
     * Gets a new target for a flight of swarm missiles that was just shot at an
     * entity and has missiles left
     *
     * @param game
     * @param aeId
     *            The attacking <code>Entity</code>
     * @param te
     *            The <code>Entity</code> that was shot at.
     * @param weaponId
     *            The <code>int</code> ID of the launcher used to fire this
     *            volley
     * @return the new target <code>Entity</code>. May return null if no new
     *         target available
     */
    public static Entity getSwarmMissileTarget(IGame game, int aeId, Entity te, int weaponId) {
        Coords coords = te.getPosition();
        Entity tempEntity = null;
        // first, check the hex of the original target
        Enumeration<Entity> entities = game.getEntities(coords);
        Vector<Entity> possibleTargets = new Vector<Entity>();
        while (entities.hasMoreElements()) {
            tempEntity = entities.nextElement();
            if (!tempEntity.getTargetedBySwarm(aeId, weaponId)) {
                // we found a target
                possibleTargets.add(tempEntity);
            }
        }
        // if there is at least one target, get a random one of them
        if (!possibleTargets.isEmpty()) {
            return possibleTargets.get(Compute.randomInt(possibleTargets.size()));
        }
        // loop through adjacent hexes
        for (int dir = 0; dir <= 5; dir++) {
            Coords tempcoords = coords.translated(dir);
            if (!game.getBoard().contains(tempcoords)) {
                continue;
            }
            if (coords.equals(tempcoords)) {
                continue;
            }
            entities = game.getEntities(tempcoords);
            if (entities.hasMoreElements()) {
                tempEntity = entities.nextElement();
                if (!tempEntity.getTargetedBySwarm(aeId, weaponId)) {
                    // we found a target
                    possibleTargets.add(tempEntity);
                }
            }
        }
        // if there is at least one target, get a random one of them
        if (!possibleTargets.isEmpty()) {
            return possibleTargets.get(Compute.randomInt(possibleTargets.size()));
        }
        return null;
    }

    public static int[] getRandomSkills(int method, int type, int level, boolean isVee) {

        int[] skills = { 4, 5 };

        // constant is the easy one
        if (method == METHOD_CONSTANT) {
            if (level == LEVEL_GREEN) {
                skills[0] = 5;
                skills[1] = 6;
            }
            if (level == LEVEL_VETERAN) {
                skills[0] = 3;
                skills[1] = 4;
            }
            if (level == LEVEL_ELITE) {
                skills[0] = 2;
                skills[1] = 3;
            }
            if ((type == TYPE_CLAN) || (type == TYPE_MD)) {
                skills[0]--;
                skills[1]--;
            }
            return skills;
        }

        // if using Taharqa's method, then the base skill level for each entity
        // is determined
        // separately
        if (method == METHOD_TAHARQA) {
            int lbonus = 0;
            if (level == LEVEL_GREEN) {
                lbonus -= 2;
            }
            if (level == LEVEL_VETERAN) {
                lbonus += 2;
            }
            if (level == LEVEL_ELITE) {
                lbonus += 4;
            }

            int lvlroll = Compute.d6(2) + lbonus;

            // restate level based on roll
            if (lvlroll < 6) {
                level = LEVEL_GREEN;
            } else if (lvlroll < 10) {
                level = LEVEL_REGULAR;
            } else if (lvlroll < 12) {
                level = LEVEL_VETERAN;
            } else {
                level = LEVEL_ELITE;
            }
        }

        // first get the bonus
        int bonus = 0;
        if (type == TYPE_CLAN) {
            if (isVee) {
                bonus--;
            } else {
                bonus++;
            }
        }
        if (type == TYPE_MD) {
            bonus++;
        }

        int gunroll = Compute.d6(1) + bonus;
        int pilotroll = Compute.d6(1) + bonus;

        int glevel = 0;
        int plevel = 0;

        switch (level) {
        case LEVEL_REGULAR:
            glevel = (int) Math.ceil(gunroll / 2.0) + 2;
            plevel = (int) Math.ceil(pilotroll / 2.0) + 2;
            break;
        case LEVEL_VETERAN:
            glevel = (int) Math.ceil(gunroll / 2.0) + 3;
            plevel = (int) Math.ceil(pilotroll / 2.0) + 3;
            break;
        case LEVEL_ELITE:
            glevel = (int) Math.ceil(gunroll / 2.0) + 4;
            plevel = (int) Math.ceil(pilotroll / 2.0) + 4;
            break;
        default:
            glevel = (int) Math.ceil((gunroll + 0.5) / 2.0);
            plevel = (int) Math.ceil((pilotroll + 0.5) / 2.0);
            if (gunroll <= 0) {
                glevel = 0;
            }
            if (pilotroll <= 0) {
                plevel = 0;
            }
        }

        skills[0] = skillLevels[0][glevel];
        skills[1] = skillLevels[1][plevel];

        return skills;
    }
    /*
    public static FighterSquadron compileSquadron(Vector<Entity> squadron) {

        //cycle through the entity vector and create a fighter squadron
        FighterSquadron fs = new FighterSquadron();
        /*
        String chassis = squadron.elementAt(0).getChassis();
        int si = 99;
        boolean alike = true;
        int armor = 0;
        int heat = 0;
        int safeThrust = 99;
        int n = 0;
        float weight = 0.0f;
        int fuel = 0;
        int bv = 0;
        double cost = 0.0;
        int nTC = 0;
        for(Entity e : squadron) {
            if(!chassis.equals(e.getChassis())) {
                alike = false;
            }
            n++;
            //names
            fs.fighters.add(e.getChassis() + " " + e.getModel());
            //armor
            armor += e.getTotalArmor();
            //heat
            heat += e.getHeatCapacity();
            //weight
            weight += e.getWeight();
            bv += e.calculateBattleValue();
            cost += e.getCost();
            //safe thrust
            if(e.getWalkMP() < safeThrust)
                safeThrust = e.getWalkMP();

            Aero a = (Aero)e;
            //si
            if(a.getSI() < si) {
                si = a.getSI();
            }

            //fuel - give the minimum fuel
            if(a.getFuel() < fuel || fuel == 0) {
                fuel = a.getFuel();
            }


            //weapons
            Mounted newmount;
            for(Mounted m : e.getEquipment() ) {

                if(m.getType() instanceof WeaponType) {
                    //first load the weapon onto the squadron
                    WeaponType wtype = (WeaponType)m.getType();
                    try{
                        newmount = fs.addEquipment(wtype, m.getLocation());
                    } catch (LocationFullException ex) {
                        System.out.println("Unable to compile weapons"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        ex.printStackTrace();
                        return fs;
                    }
                    //skip to the next if it has no AT class
                    if(wtype.getAtClass() == WeaponType.CLASS_NONE) {
                        continue;
                    }

                    //now find the right bay
                    Mounted bay = fs.getFirstBay(wtype, newmount.getLocation(), newmount.isRearMounted());
                    //if this is null, then I should create a new bay
                    if(bay == null) {
                        EquipmentType newBay = WeaponType.getBayType(wtype.getAtClass());
                        try{
                            bay = fs.addEquipment(newBay, newmount.getLocation());
                        } catch (LocationFullException ex) {
                            System.out.println("Unable to compile weapons"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                            ex.printStackTrace();
                            return fs;
                        }
                    }
                    //now add the weapon to the bay
                    bay.addWeaponToBay(fs.getEquipmentNum(newmount));
                } else {
                    //just add the equipment normally
                    try{
                        //check if this is a TC
                        if (m.getType() instanceof MiscType && m.getType().hasFlag(MiscType.F_TARGCOMP)) {
                            nTC++;
                        }
                        fs.addEquipment(m.getType(), m.getLocation());
                    } catch (LocationFullException ex) {
                        System.out.println("Unable to add equipment"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                        ex.printStackTrace();
                        return fs;
                    }
                }
            }
        }

        armor = (int)Math.round(armor / 10.0);

        fs.setArmor(armor);
        fs.set0Armor(armor);
        fs.setHeatSinks(heat);
        fs.setOriginalWalkMP(safeThrust);
        fs.setN0Fighters(n);
        fs.setNFighters(n);
        fs.autoSetThresh();
        fs.setWeight(weight);
        fs.set0SI(si);
        fs.setCost(cost);
        fs.setFuel(fuel);

        if(nTC >= n) {
            fs.setHasTC(true);
        }

        //if all the same chassis, name by chassis
        //otherwise name by weight
        if(alike) {
            fs.setChassis(chassis + " Squadron");
        } else {
            int aveWeight = Math.round(weight/n);
            if(aveWeight <= 45) {
                fs.setChassis("Mixed Light Squadron");
            } else if(aveWeight <= 70) {
                fs.setChassis("Mixed Medium Squadron");
            } else {
                fs.setChassis("Mixed Heavy Squadron");
            }
        }
        fs.setModel("");
        fs.loadAllWeapons();
        fs.setRapidFire();

        return fs;
    }
    */



    /****STUFF FOR VECTOR MOVEMENT CALCULATIONS***/
    public static Coords getFinalPosition(Coords curpos, int[] v) {

        if((v == null) || (v.length != 6)) {
            return curpos;
        }

        //step through each vector and move the direction indicated
        int thrust = 0;
        Coords endpos = curpos;
        for(int dir = 0; dir < 6; dir++) {
            thrust = v[dir];
            while(thrust > 0) {
                endpos = endpos.translated(dir);
                thrust--;
            }
        }

        return endpos;
    }

    /**
     * method to change a set of active vectors for a one-point thrust
     * expenditure in the giving facing
     * @param v
     * @param facing
     * @return
     */
    public static int[] changeVectors(int[] v, int facing) {

        if((v == null) || (v.length != 6)) {
            return v;
        }

        //first look at opposing vectors
        int oppv = facing + 3;
        if(oppv > 5) {
            oppv -= 6;
        }
        //is this vector active
        if(v[oppv] > 0) {
            //then decrement it by one and return
            v[oppv]--;
            return v;
        }

        //now check oblique vectors
        int oblv1 = facing + 2;
        if(oblv1 > 5) {
            oblv1 -= 6;
        }
        int oblv2 = facing - 2;
        if(oblv2 < 0) {
            oblv2 += 6;
        }

        //check both of these and if either is active
        //deal with it and then return
        if((v[oblv1] > 0) || (v[oblv2] > 0)) {

            int newface = facing + 1;
            if(newface > 5) {
                newface = 0;
            }
            if(v[oblv1] > 0) {
                v[oblv1]--;
                v[newface]++;
            }

            newface = facing - 1;
            if(newface < 0) {
                newface = 0;
            }
            if(v[oblv2] > 0) {
                v[oblv2]--;
                v[newface]++;
            }
            return v;
        }

        //if nothing was found, then just increase velocity in this vector
        v[facing]++;
        return v;
    }

    /**
     * compare two vectors and determine if they are the same
     * @param v1
     * @param v2
     * @return
     */
    public static boolean sameVectors(int[] v1, int[] v2) {

        for(int i = 0; i<6; i++) {
            if(v1[i] != v2[i]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get the net velocity of two aeros for ramming attacks
     */
    public static int getNetVelocity(Coords src, Aero te, int avel, int tvel) {
        int angle = te.sideTableRam(src);

        switch(angle) {
        case Aero.RAM_TOWARD_DIR:
            return Math.max(avel+tvel,1);
        case Aero.RAM_TOWARD_OBL:
            return Math.max(avel+(tvel/2),1);
        case Aero.RAM_AWAY_OBL:
            return Math.max(avel-(tvel/2),1);
        case Aero.RAM_AWAY_DIR:
            return Math.max(avel-tvel,1);
        }
        return 0;
    }

    /**
     * Method replicates the Non-Conventional Damage against Infantry damage table
     * as well as shifting for direct blows.
     * also adjust for non-infantry damaging mechanized infantry
     * @param damage
     * @param mos
     * @param damageType
     * @return
     */
    public static int directBlowInfantryDamage(double damage, int mos, int damageType, boolean isNonInfantryAgainstMechanized){

        damageType += mos;

        switch(damageType){
        case Compute.WEAPON_DIRECT_FIRE:
            damage /= 10;
            break;
        case Compute.WEAPON_CLUSTER_BALLISTIC:
            damage /= 10;
            damage++;
            break;
        case Compute.WEAPON_PULSE:
            damage /= 10;
            damage += 2;
            break;
        case Compute.WEAPON_CLUSTER_MISSILE:
            damage /= 5;
            break;
        case Compute.WEAPON_CLUSTER_MISSILE_1D6:
            damage /=5;
            damage += Compute.d6();
            break;
        case Compute.WEAPON_CLUSTER_MISSILE_2D6:
            damage /=5;
            damage += Compute.d6(2);
            break;
        case Compute.WEAPON_CLUSTER_MISSILE_3D6:
            damage /=5;
            damage += Compute.d6(3);
            break;
        }
        damage = Math.ceil(damage);
        if (isNonInfantryAgainstMechanized) {
            damage *= 2;
        }
        return (int)damage;
    }

    /**
     * Method computes how much damage a dial down weapon has done
     * @param weapon
     * @param wtype
     * @returnnew damage
     */
    public static int dialDownDamage(Mounted weapon, WeaponType wtype){
        return Compute.dialDownDamage(weapon, wtype, 1);
    }

    /**
     * Method computes how much damage a dial down weapon has done
     * @param weapon
     * @param wtype
     * @param range
     * @return new damage
     */
    public static int dialDownDamage(Mounted weapon, WeaponType wtype, int range){
        int toReturn = wtype.getDamage(range);

        if ( !wtype.hasModes() ) {
            return toReturn;
        }

        String damage = weapon.curMode().getName();

        //Vehicle flamers have damage and heat modes so lets make sure this is an actual dial down Damage.
        if ( (damage.trim().toLowerCase().indexOf("damage") == 0) && (damage.trim().length() > 6)){
            toReturn = Integer.parseInt(damage.substring(6).trim());
        }

        return Math.min(wtype.getDamage(range), toReturn);

    }

    /**
     * Method computes how much heat a dial down weapon generates
     * @param weapon
     * @param wtype
     * @return Heat, minimum of 1;
     */
    public static int dialDownHeat(Mounted weapon, WeaponType wtype){
        return Compute.dialDownHeat(weapon, wtype,1);
    }

    /**
     * Method computes how much heat a dial down weapon generates
     * @param weapon
     * @param wtype
     * @param range
     * @return Heat, minimum of 1;
     */
    public static int dialDownHeat(Mounted weapon, WeaponType wtype, int range){
        int toReturn = wtype.getHeat();

        if ( !wtype.hasModes() ) {
            return toReturn;
        }

        int damage = wtype.getDamage(range);
        int newDamage = Compute.dialDownDamage(weapon, wtype, range);


        toReturn = Math.max(1, wtype.getHeat()-Math.max(0,damage-newDamage));
        return toReturn;

    }

    /**
     * @param ae - attacking entity
     * @param te - targeted entity
     * @return a vector of all the entities that are adjacent to the targeted entity and would fall along the angle
     *         of attack
     */
    public static ArrayList<Entity> getAdjacentEntitiesAlongAttack(Coords aPos, Coords tPos, IGame game) {
        ArrayList<Entity> entities = new ArrayList<Entity>();
        ArrayList<Coords> coords = Coords.intervening(aPos, tPos);
        // loop through all intervening coords
        for (Coords c : coords) {
            //must be adjacent to the target
            if((c.distance(tPos) > 1) || c.equals(tPos)) {
                continue;
            }
            //now lets add all the entities here
            for (Enumeration<Entity> i = game.getEntities(c); i.hasMoreElements();) {
                Entity en = i.nextElement();
                entities.add(en);
            }
        }
        return entities;
    }
} // End public class Compute

