gt: 			if (gameTimer == maxGameTime / 2)
context: /**
   * UltimateArena - fully customizable PvP arenas
   * Copyright (C) 2012 - 2015 MineSworn
   * Copyright (C) 2013 - 2015 dmulloy2
   *
   * 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 3 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.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  package net.dmulloy2.ultimatearena.arenas;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.logging.Level;
  
  import net.dmulloy2.chat.BaseComponent;
  import net.dmulloy2.chat.ChatUtil;
  import net.dmulloy2.chat.ComponentSerializer;
  import net.dmulloy2.types.CustomScoreboard;
  import net.dmulloy2.types.Reloadable;
  import net.dmulloy2.ultimatearena.Config;
  import net.dmulloy2.ultimatearena.UltimateArena;
  import net.dmulloy2.ultimatearena.api.ArenaType;
  import net.dmulloy2.ultimatearena.api.event.ArenaConcludeEvent;
  import net.dmulloy2.ultimatearena.api.event.ArenaJoinEvent;
  import net.dmulloy2.ultimatearena.api.event.ArenaLeaveEvent;
  import net.dmulloy2.ultimatearena.api.event.ArenaSpawnEvent;
  import net.dmulloy2.ultimatearena.api.event.ArenaStartEvent;
  import net.dmulloy2.ultimatearena.arenas.pvp.PvPArena;
  import net.dmulloy2.ultimatearena.gui.ClassSelectionGUI;
  import net.dmulloy2.ultimatearena.types.ArenaClass;
  import net.dmulloy2.ultimatearena.types.ArenaConfig;
  import net.dmulloy2.ultimatearena.types.ArenaFlag;
  import net.dmulloy2.ultimatearena.types.ArenaLocation;
  import net.dmulloy2.ultimatearena.types.ArenaPlayer;
  import net.dmulloy2.ultimatearena.types.ArenaSpectator;
  import net.dmulloy2.ultimatearena.types.ArenaZone;
  import net.dmulloy2.ultimatearena.types.KillStreak;
  import net.dmulloy2.ultimatearena.types.LeaveReason;
  import net.dmulloy2.ultimatearena.types.Permission;
  import net.dmulloy2.ultimatearena.types.Team;
  import net.dmulloy2.ultimatearena.types.WinCondition;
  import net.dmulloy2.util.CompatUtil;
  import net.dmulloy2.util.FormatUtil;
  import net.dmulloy2.util.Util;
  
  import org.apache.commons.lang.WordUtils;
  import org.bukkit.ChatColor;
  import org.bukkit.GameMode;
  import org.bukkit.Location;
  import org.bukkit.Material;
  import org.bukkit.World;
  import org.bukkit.entity.Entity;
  import org.bukkit.entity.Player;
  import org.bukkit.event.HandlerList;
  import org.bukkit.event.Listener;
  import org.bukkit.potion.PotionEffect;
  import org.bukkit.scheduler.BukkitRunnable;
  
  import lombok.Getter;
  import lombok.Setter;
  
  /**
   * Base arena class. Can be extended to create custom arena types.
   * <p>
   * This class must be extended to function, since it does not create a
   * functional arena on its own. It does, however, provide useful methods that
   * can be used to create a functional arena.
   * <p>
   * The simplest example of a functional arena is the {@link PvPArena}
   *
   * @author dmulloy2
   */
  
  @Getter @Setter
  public abstract class Arena implements Reloadable, Listener
  {
  	public enum Mode
  	{
  		LOBBY, INGAME, STOPPING, IDLE, DISABLED
  	}
  
  	protected List<ArenaPlayer> active;
  	protected List<ArenaPlayer> inactive;
  	protected List<ArenaPlayer> toReward;
  	protected List<ArenaSpectator> spectators;
  	protected String lastJoin;
  
  	protected List<ArenaPlayer> leaderboard;
  	protected List<String> finalLeaderboard;
  	protected int leaderboardIndex;
  
  	protected List<ArenaFlag> flags;
  	protected List<ArenaLocation> spawns;
  
  	private String defaultClass;
  	private List<String> blacklistedClasses;
  	private List<String> whitelistedClasses;
  
  	private Map<Team, List<String>> mandatedClasses;
  	private Map<Integer, List<KillStreak>> killStreaks;
  
  	protected WinCondition winCondition;
  	protected Team winningTeam;
  
  	protected int broadcastTimer = 45;
  	protected int maxPlayers = 24;
  	protected int minPlayers = 1;
  	protected int maxDeaths = 1;
  
  	protected int startingAmount;
  	protected int maxGameTime;
  	protected int startTimer;
  	protected int gameTimer;
  	protected int redTeamSize;
  	protected int blueTeamSize;
  	protected int announced;
  	protected int gracePeriod;
  
  	protected boolean allowTeamKilling;
  	protected boolean rewardBasedOnXp;
  	protected boolean pauseStartTimer;
  	protected boolean joinInProgress;
  	protected boolean countMobKills;
  	protected boolean forceBalance;
  	protected boolean giveRewards;
  	protected boolean limitSpam;
  
  	protected boolean disabled;
  	protected boolean stopped;
  	protected boolean started;
  	protected boolean inLobby;
  	protected boolean inGame;
  
  	protected String name;
  
  	protected Mode gameMode;
  
  	protected final World world;
  	protected final ArenaZone az;
  	protected final ArenaType type;
  	protected final UltimateArena plugin;
  
  	/**
  	 * Base {@link Arena} constructor. All constructors must reference this.
  	 *
  	 * @param az {@link ArenaZone} to base the {@link Arena} around.
  	 */
  	public Arena(ArenaZone az)
  	{
  		this.az = az;
  		this.plugin = az.getPlugin();
  		this.name = az.getName();
  		this.type = az.getType();
  		this.world = az.getWorld();
  		this.az.setTimesPlayed(az.getTimesPlayed() + 1);
  
  		this.active = new ArrayList<>();
  		this.inactive = new ArrayList<>();
  		this.spectators = new ArrayList<>();
  
  		this.flags = new ArrayList<>();
  		this.spawns = new ArrayList<>();
  
  		this.gameMode = Mode.LOBBY;
  
  		this.startTimer = az.getConfig().getLobbyTime();
  		this.inLobby = true;
  
  		plugin.getServer().getPluginManager().registerEvents(this, plugin);
  		this.reload();
  	}
  
  	/**
  	 * Reloads the Arena's settings.
  	 */
  	@Override
  	public final void reload()
  	{
  		this.allowTeamKilling = az.getConfig().isAllowTeamKilling();
  		this.countMobKills = az.getConfig().isCountMobKills();
  		this.rewardBasedOnXp = az.getConfig().isRewardBasedOnXp();
  		this.giveRewards = az.getConfig().isGiveRewards();
  		this.forceBalance = az.getConfig().isForceBalance();
  		this.joinInProgress = az.getConfig().isJoinInProgress();
  
  		this.maxGameTime = az.getConfig().getGameTime();
  		this.gameTimer = az.getConfig().getGameTime();
  		this.maxDeaths = az.getConfig().getMaxDeaths();
  		this.maxPlayers = az.getConfig().getMaxPlayers();
  		this.minPlayers = az.getConfig().getMinPlayers();
  		this.gracePeriod = az.getConfig().getGracePeriod();
  
  		this.defaultClass = az.getConfig().getDefaultClass();
  
  		this.killStreaks = az.getConfig().getKillStreaks();
  		this.winCondition = az.getConfig().getWinCondition();
  		this.blacklistedClasses = az.getConfig().getBlacklistedClasses();
  		this.whitelistedClasses = az.getConfig().getWhitelistedClasses();
  		this.mandatedClasses = az.getConfig().getMandatedClasses();
  
  		onReload();
  	}
  
  	/**
  	 * Called when the Arena is reloaded.
  	 */
  	public void onReload() { }
  
  	/**
  	 * Adds a player to the arena.
  	 *
  	 * @param player {@link Player} to add to an arena
  	 * @param teamId Team name/id
  	 */
  	public final void addPlayer(final Player player, String teamId)
  	{
  		player.sendMessage(plugin.getPrefix() + FormatUtil.format(getMessage("joiningArena"), name));
  
  		final ArenaPlayer ap = new ArenaPlayer(player, this, plugin);
  
  		// API - cancellable join event
  		ArenaJoinEvent event = new ArenaJoinEvent(this, ap);
  		plugin.getServer().getPluginManager().callEvent(event);
  		if (event.isCancelled())
  		{
  			ap.sendMessage(event.getCancelMessage() != null ? event.getCancelMessage() : "&3Unexpected exit!");
  			ap.clear();
  			return;
  		}
  
  		// API - onJoin
  		onJoin(ap);
  
  		// Determine their team
  		Team team = teamId == null ? getTeam() : Team.get(teamId);
  		ap.setTeam(team);
  
  		// Teleport the player to the lobby spawn
  		spawnLobby(ap);
  
  		// Save vital data
  		ap.savePlayerData();
  
  		player.setMetadata("UA", plugin.getIdentifier());
  
  		ap.clearInventory();
  
  		player.setGameMode(GameMode.SURVIVAL);
  
  		// Heal up the Player
  		player.setFoodLevel(20);
  		player.setFireTicks(0);
  		player.setHealth(20);
  
  		// Don't allow flight
  		player.setAllowFlight(false);
  		player.setFlySpeed(0.1F);
  		player.setFlying(false);
  
  		// Disable Essentials god mode
  		if (plugin.isEssentialsEnabled())
  			plugin.getEssentialsHandler().disableGodMode(player);
  
  		ap.clearPotionEffects();
  
  		decideHat(ap);
  
  		active.add(ap);
  		updateTeams();
  
  		// Alert the other players
  		tellPlayers(getMessage("joinedArena"), ap.getName(), active.size(), maxPlayers);
  		this.lastJoin = ap.getName();
  
  		// Open Class Selector a second later
  		if (Config.classSelectorAutomatic)
  		{
  			new BukkitRunnable()
  			{
  				@Override
  				public void run()
  				{
  					ClassSelectionGUI csGUI = new ClassSelectionGUI(plugin, ap, started);
  					plugin.getGuiHandler().open(player, csGUI);
  				}
  			}.runTaskLater(plugin, 20L);
  		}
  		else if (started)
  		{
  			spawn(ap);
  		}
  
  		this.leaderboard = null;
  	}
  
  	/**
  	 * Decide a player's hat
  	 * @param ap Player to give hat to
  	 */
  	public void decideHat(ArenaPlayer ap)
  	{
  		ap.decideHat(false);
  	}
  
  	/**
  	 * Gets a list of available classes for a given Team.
  	 * 
  	 * @param team Player team
  	 * @return Available classes
  	 */
  	public List<ArenaClass> getAvailableClasses(Team team)
  	{
  		List<String> classNames = mandatedClasses.get(team);
  		if (classNames != null && classNames.size() > 0)
  		{
  			List<ArenaClass> classes = new ArrayList<>();
  			for (String name : classNames)
  			{
  				ArenaClass ac = plugin.getArenaClass(name);
  				if (ac != null)
  					classes.add(ac);
  			}
  
  			return classes;
  		}
  
  		return plugin.getClasses();
  	}
  
  	/**
  	 * Called when an {@link ArenaPlayer} joins the arena.
  	 *
  	 * @param ap {@link ArenaPlayer} who joined
  	 */
  	public void onJoin(ArenaPlayer ap) { }
  
  	/**
  	 * Gets which team a new player should be on.
  	 * <p>
  	 * Can be overriden in certain cases.
  	 *
  	 * @return The team, defaults to RED.
  	 */
  	public Team getTeam()
  	{
  		return Team.RED;
  	}
  
  	/**
  	 * Announces the arena's existance and reminds players to join.
  	 */
  	public final void announce()
  	{
  		if (! Config.globalMessages)
  			return;
  
  		// Allow players to click and insert into chat
  		String clickToJoin = getMessage("clickToJoin");
  		BaseComponent[] components = clickToJoin.isEmpty() ? null : ComponentSerializer.parse(FormatUtil.format(clickToJoin.replace("%s", name)));
  
  		for (Player player : Util.getOnlinePlayers())
  		{
  			if (! plugin.isInArena(player))
  			{
  				if (plugin.getPermissionHandler().hasPermission(player, Permission.JOIN))
  				{
  					String message;
  					
  					if (announced == 0)
  						message = FormatUtil.format(getMessage("arenaCreated"), type.getStylizedName());
  					else
  						message = FormatUtil.format(getMessage("hurryAndJoin"), type.getStylizedName());
  
  					if (! message.isEmpty())
  						player.sendMessage(plugin.getPrefix() + message);
  
  					if (components != null)
  						ChatUtil.sendMessage(player, components);
  				}
  			}
  		}
  
  		announced++;
  	}
  
  	/**
  	 * Gets the team with the least amount of players on it.
  	 *
  	 * @return The team with the least amount of players
  	 */
  	public final Team getBalancedTeam()
  	{
  		return redTeamSize > blueTeamSize ? Team.BLUE : Team.RED;
  	}
  
  	/**
  	 * A simple team check.
  	 *
  	 * @return True if there are people on both teams, false if not
  	 */
  	public boolean simpleTeamCheck()
  	{
  		return (redTeamSize != 0 && blueTeamSize != 0) || startingAmount < 1;
  	}
  
  	/**
  	 * Gets a {@link Player}'s {@link ArenaPlayer} instance.
  	 * <p>
  	 * Every player who has joined this arena will have an ArenaPlayer instance.
  	 * It is important to note, however, that players who are out will still
  	 * have arena player instances until the arena concludes. Use
  	 * {@code player.isOut()} to make sure they're still in the arena.
  	 *
  	 * @param player Player
  	 * @param checkInactive Whether or not to check the inactive list as well
  	 * @return The player's ArenaPlayer instance, or null if not found
  	 */
  	public final ArenaPlayer getArenaPlayer(Player player, boolean checkInactive)
  	{
  		for (ArenaPlayer ap : getActivePlayers())
  		{
  			if (player.getUniqueId().equals(ap.getUniqueId()))
  				return ap;
  		}
  
  		if (checkInactive)
  		{
  			for (ArenaPlayer ap : getInactivePlayers())
  			{
  				if (player.getUniqueId().equals(ap.getUniqueId()))
  					return ap;
  			}
  		}
  
  		return null;
  	}
  
  	/**
  	 * Alias for {@link #getArenaPlayer(Player, boolean)}.
  	 * <p>
  	 * Has the same effect as <code>getArenaPlayer(player, false)</code>
  	 *
  	 * @param player Player
  	 */
  	public final ArenaPlayer getArenaPlayer(Player player)
  	{
  		return getArenaPlayer(player, false);
  	}
  
  	/**
  	 * Spawns all players in the arena.
  	 */
  	public final void spawnAll()
  	{
  		plugin.debug("Spawning players for arena {0}", name);
  
  		for (ArenaPlayer ap : active)
  		{
  			spawn(ap);
  		}
  	}
  
  	/**
  	 * Gets the spawn for an {@link ArenaPlayer}.
  	 * <p>
  	 * Can be overriden under certain circumstances
  	 *
  	 * @param ap {@link ArenaPlayer} instance
  	 * @return Their spawn
  	 */
  	public Location getSpawn(ArenaPlayer ap)
  	{
  		ArenaLocation loc;
  		if (isInLobby())
  		{
  			loc = az.getLobbyREDspawn();
  			if (ap.getTeam() == Team.BLUE)
  				loc = az.getLobbyBLUspawn();
  		}
  		else
  		{
  			loc = az.getTeam1spawn();
  			if (ap.getTeam() == Team.BLUE)
  				loc = az.getTeam2spawn();
  		}
  
  		return loc.getLocation();
  	}
  
  	/**
  	 * Spawns a player in the {@link Arena}.
  	 *
  	 * @param ap Player to spawn
  	 * @param alreadySpawned Whether or not they've already spawned
  	 */
  	public final void spawn(ArenaPlayer ap, boolean alreadySpawned)
  	{
  		plugin.debug("Attempting to spawn player {0}", ap.getName());
  
  		if (! stopped)
  		{
  			if (ap.getDeaths() < maxDeaths)
  			{
  				Location loc = getSpawn(ap);
  				if (loc != null)
  				{
  					plugin.debug("Spawning player {0}", ap.getName());
  
  					ArenaSpawnEvent event = new ArenaSpawnEvent(this, ap, loc);
  					plugin.getServer().getPluginManager().callEvent(event);
  
  					ap.teleport(event.getLocation());
  					ap.spawn();
  
  					if (! alreadySpawned)
  					{
  						onSpawn(ap);
  					}
  				}
  				else
  				{
  					// We couldn't find a spawnpoint for some reason :(
  					ap.sendMessage(getMessage("nullSpawnpoint"));
  					ap.leaveArena(LeaveReason.ERROR);
  				}
  			}
  			else
  			{
  				// If they've reached the death cap, remove them
  				ap.leaveArena(LeaveReason.DEATHS);
  			}
  		}
  	}
  
  	/**
  	 * Alias for {@link #spawn(ArenaPlayer, boolean)}.
  	 * <p>
  	 * Has the same effect of <code>spawn(player, false)</code>
  	 *
  	 * @param ap Player to spawn
  	 */
  	public final void spawn(ArenaPlayer ap)
  	{
  		spawn(ap, false);
  	}
  
  	/**
  	 * Called when a player is spawned.
  	 *
  	 * @param ap {@link ArenaPlayer} who was spawned
  	 */
  	public void onSpawn(ArenaPlayer ap) { }
  
  	/**
  	 * Spawns an {@link ArenaPlayer} into the lobby.
  	 *
  	 * @param ap {@link ArenaPlayer} to spawn
  	 */
  	public final void spawnLobby(ArenaPlayer ap)
  	{
  		Location loc = getSpawn(ap);
  		if (loc != null)
  		{
  			plugin.debug("Spawning player {0} in the lobby", ap.getName());
  
  			ap.teleport(loc);
  		}
  		else
  		{
  			ap.sendMessage(getMessage("nullSpawnpoint"));
  			ap.leaveArena(LeaveReason.ERROR);
  		}
  	}
  
  	public final void clearCache()
  	{
  		this.leaderboard = null;
  	}
  
  	/**
  	 * Called when a player dies.
  	 *
  	 * @param pl {@link ArenaPlayer} who died
  	 */
  	public void onPlayerDeath(ArenaPlayer pl) { }
  
  	/**
  	 * Default rewarding system. May be overriden in some cases.
  	 *
  	 * @param ap {@link ArenaPlayer} to reward
  	 */
  	public void reward(ArenaPlayer ap)
  	{
  		if (ap != null && az.getConfig().isGiveRewards())
  			az.giveRewards(ap);
  	}
  
  	/**
  	 * Rewards an entire team.
  	 *
  	 * @param team Team to reward
  	 */
  	public final void rewardTeam(Team team)
  	{
  		if (az.getConfig().isGiveRewards())
  		{
  			for (ArenaPlayer ap : toReward)
  			{
  				if (ap.isCanReward())
  				{
  					if (ap.getTeam() == team || team == null)
  						reward(ap);
  				}
  			}
  		}
  
  		toReward.clear();
  	}
  
  	/**
  	 * Sets the winning team.
  	 *
  	 * @param team Winning team
  	 */
  	public final void setWinningTeam(Team team)
  	{
  		this.toReward = new ArrayList<>();
  
  		for (ArenaPlayer ap : active)
  		{
  			ap.setCanReward(false);
  			if (ap.getTeam() == team || team == null)
  			{
  				ap.setCanReward(true);
  				toReward.add(ap);
  			}
  		}
  
  		this.winningTeam = team;
  	}
  
  	/**
  	 * Stops the arena if empty.
  	 *
  	 * @return Whether or not the arena is empty
  	 */
  	public final boolean checkEmpty()
  	{
  		boolean ret = isEmpty();
  		if (ret)
  			stop();
  
  		return ret;
  	}
  
  	/**
  	 * Checks if the arena is empty.
  	 *
  	 * @return Whether or not the arena is empty
  	 */
  	public final boolean isEmpty()
  	{
  		return isInGame() && active.size() <= 1;
  	}
  
  	/**
  	 * Tells all active players in the arena a message.
  	 *
  	 * @param string Base message
  	 * @param objects Objects to format in
  	 */
  	public final void tellPlayers(String string, Object... objects)
  	{
  		if (string.isEmpty()) return;
  
  		for (ArenaPlayer ap : active)
  		{
  			ap.sendMessage(string, objects);
  		}
  
  		for (ArenaSpectator as : spectators)
  		{
  			as.sendMessage(string, objects);
  		}
  	}
  
  	/**
  	 * Tells all players a message.
  	 * <p>
  	 * Includes inactive players
  	 *
  	 * @param string Base message
  	 * @param objects Objects to format in
  	 */
  	public final void tellAllPlayers(String string, Object... objects)
  	{
  		if (string.isEmpty()) return;
  
  		tellPlayers(string, objects);
  
  		for (ArenaPlayer ap : inactive)
  		{
  			if (ap != null && ap.isOnline())
  			{
  				ap.sendMessage(string, objects);
  			}
  		}
  	}
  
  	/**
  	 * Kills all players within a certain radius of a {@link Location}
  	 *
  	 * @param loc Center {@link Location}
  	 * @param radius Radius to kill within
  	 */
  	public final void killAllNear(Location loc, double radius)
  	{
  		plugin.debug("Killing all players near {0} in a radius of {1}", Util.locationToString(loc), radius);
  
  		double squared = radius * radius;
  
  		for (ArenaPlayer ap : active)
  		{
  			Player player = ap.getPlayer();
  			if (player.getHealth() > 0.0D && player.getLocation().distanceSquared(loc) < squared)
  				player.setHealth(0.0D);
  		}
  	}
  
  	/**
  	 * Returns a random spawn for an {@link ArenaPlayer}.
  	 *
  	 * @param ap {@link ArenaPlayer} to get spawn for
  	 */
  	public Location getRandomSpawn(ArenaPlayer ap)
  	{
  		plugin.debug("Getting a random spawn for {0}", ap.getName());
  
  		if (! spawns.isEmpty())
  		{
  			return spawns.get(Util.random(spawns.size())).getLocation();
  		}
  
  		return null;
  	}
  
  	/**
  	 * Handles an {@link ArenaPlayer}'s kill streak (if applicable)
  	 *
  	 * @param ap {@link ArenaPlayer} to handle kill streak for
  	 */
  	public final void handleKillStreak(ArenaPlayer ap)
  	{
  		if (killStreaks.isEmpty())
  			return;
  
  		if (killStreaks.containsKey(ap.getKillStreak()))
  		{
  			List<KillStreak> streaks = killStreaks.get(ap.getKillStreak());
  			for (KillStreak streak : streaks)
  			{
  				if (streak != null)
  					streak.perform(ap);
  			}
  		}
  	}
  
  	/**
  	 * Disables this arena.
  	 */
  	public final void disable()
  	{
  		tellPlayers(getMessage("hasBeenDisabled"));
  
  		this.gameTimer = -1;
  
  		stop();
  
  		this.disabled = true;
  		this.gameMode = Mode.DISABLED;
  
  		updateSigns();
  		onDisable();
  	}
  
  	/**
  	 * Called when the arena is disabled.
  	 */
  	public void onDisable() { }
  
  	/**
  	 * Ends the arena.
  	 */
  	public final void stop()
  	{
  		if (stopped)
  			return; // No need to stop multiple times
  
  		plugin.log("Stopping arena {0}!", name);
  
  		this.inGame = false;
  		this.stopped = true;
  
  		this.gameMode = Mode.STOPPING;
  
  		clearSigns();
  		onStop();
  
  		announceWinner();
  
  		for (ArenaPlayer ap : getActivePlayers())
  		{
  			endPlayer(ap, LeaveReason.END_GAME);
  		}
  
  		for (ArenaSpectator as : spectators)
  		{
  			as.endPlayer();
  		}
  
  		clearEntities();
  		clearMaterials();
  
  		conclude(120L);
  	}
  
  	/**
  	 * Called when the arena is stopped.
  	 */
  	public void onStop() { }
  
  	private void conclude(long delay)
  	{
  		if (delay <= 0 || plugin.isStopping())
  		{
  			conclude();
  			return;
  		}
  
  		new BukkitRunnable()
  		{
  			@Override
  			public void run()
  			{
  				conclude();
  			}
  		}.runTaskLater(plugin, 120L);
  	}
  
  	private void conclude()
  	{
  		// API - conclude event
  		ArenaConcludeEvent event = new ArenaConcludeEvent(this);
  		plugin.getServer().getPluginManager().callEvent(event);
  
  		if (! disabled)
  			this.gameMode = Mode.IDLE;
  
  		plugin.removeActiveArena(this);
  		plugin.broadcast(getMessage("concluded"), WordUtils.capitalize(name));
  
  		plugin.getSignHandler().onArenaCompletion(this);
  		plugin.getSignHandler().updateSigns(az);
  
  		HandlerList.unregisterAll(this);
  	}
  
  	/**
  	 * Ends an {@link ArenaPlayer}.
  	 *
  	 * @param ap Player to end
  	 * @param reason Reason they're being ended
  	 */
  	public final void endPlayer(ArenaPlayer ap, LeaveReason reason)
  	{
  		plugin.debug("Ending player {0}, reason: {1}", ap.getName(), reason);
  
  		// API - leave event
  		ArenaLeaveEvent event = new ArenaLeaveEvent(this, ap);
  		plugin.getServer().getPluginManager().callEvent(event);
  
  		// Reset them
  		ap.reset();
  		ap.teleport(ap.getSpawnBack());
  
  		ap.setOut(true);
  
  		final Player player = ap.getPlayer();
  
  		// Invisibility fix
  		if (Config.fixInvisibility)
  		{
  			new BukkitRunnable()
  			{
  				@Override
  				public void run()
  				{
  					for (ArenaPlayer active : getActivePlayers())
  					{
  						if (active.getPlayer() != null)
  						{
  							CompatUtil.showPlayer(active.getPlayer(), plugin, player);
  						}
  					}
  				}
  			}.runTaskLater(plugin, 5L);
  		}
  
  		// Remove from lists
  		active.remove(ap);
  		if (reason != LeaveReason.QUIT)
  			inactive.add(ap);
  		updateTeams();
  
  		// Add them to the final leaderboard if applicable
  		if (finalLeaderboard != null)
  		{
  			try
  			{
  				finalLeaderboard.set(leaderboardIndex, ap.getName());
  				leaderboardIndex--;
  			} catch (IndexOutOfBoundsException ignored) { }
  		}
  
  		// Invalidate leaderboard cache
  		this.leaderboard = null;
  
  		// Let everyone know why
  		switch (reason)
  		{
  			case COMMAND:
  			case QUIT:
  				tellPlayers(getMessage("leaveBroadcast"), ap.getName());
  				break;
  			case DEATHS:
  				tellPlayers(getMessage("elimination"), ap.getName());
  				break;
  			case ERROR:
  				tellPlayers(getMessage("errorBroadcast"), ap.getName());
  				break;
  			case GENERIC:
  			case KICK:
  				tellPlayers(getMessage("kickBroadcast"), ap.getName());
  				break;
  			case END_GAME:
  			case POWER:
  				break;
  		}
  
  		if (reason != LeaveReason.END_GAME && active.size() > 1)
  			tellPlayers(getMessage("playersRemaining"), active.size());
  
  		// API - onPlayerEnd
  		onPlayerEnd(ap);
  	}
  
  	/**
  	 * Called after an {@link ArenaPlayer} is ended.
  	 * 
  	 * @param ap Ended ArenaPlayer
  	 */
  	public void onPlayerEnd(ArenaPlayer ap) { }
  
  	/**
  	 * Basic timer checker.
  	 * <p>
  	 * Can not be overriden.
  	 */
  	public final void checkTimers()
  	{
  		if (stopped)
  			return;
  
  		if (! pauseStartTimer)
  		{
  			startTimer--;
  			broadcastTimer--;
  		}
  
  		if (startTimer <= 0)
  		{
  			start();
  			gameTimer--;
  		}
  		else
  		{
  			if (broadcastTimer < 0)
  			{
  				broadcastTimer = 45;
  				announce();
  			}
  		}
  
  		// End the game
  		if (gameTimer <= 0)
  		{
  			tellPlayers(getMessage("outOfTime"));
  
  			onPreOutOfTime();
  			stop();
  			onOutOfTime();
  		}
  	}
  
  	/**
  	 * Called right before the arena runs out of time.
  	 */
  	public void onPreOutOfTime() { }
  
  	/**
  	 * Called after the arena runs out of time.
  	 */
  	public void onOutOfTime() { }
  
  	/**
  	 * Starts the arena.
  	 * <p>
  	 * Can not be overriden.
  	 */
  	public final void start()
  	{
  		if (! started)
  		{
  			// Make sure there are enough players
  			if (active.size() < minPlayers)
  			{
  				tellPlayers(getMessage("notEnoughPeople"), minPlayers);
  				stop();
  				return;
  			}
  
  			// API - start event
  			ArenaStartEvent event = new ArenaStartEvent(this);
  			plugin.getServer().getPluginManager().callEvent(event);
  
  			// API - onStart
  			onStart();
  
  			// Balance teams if applicable
  			if (forceBalance && lastJoin != null)
  			{
  				if (redTeamSize != blueTeamSize)
  				{
  					Player extra = Util.matchPlayer(lastJoin);
  					if (extra != null)
  					{
  						ArenaPlayer ap = getArenaPlayer(extra);
  						if (ap != null)
  						{
  							ap.leaveArena(LeaveReason.KICK);
  							ap.sendMessage(getMessage("fairnessKick"));
  						}
  					}
  				}
  			}
  
  			plugin.log("Starting arena {0} with {1} players", name, active.size());
  
  			this.started = true;
  			this.inGame = true;
  			this.inLobby = false;
  
  			this.gameMode = Mode.INGAME;
  
  			this.startingAmount = active.size();
  
  			// Setup the leaderboard
  			this.finalLeaderboard = new ArrayList<>(startingAmount);
  			Collections.fill(finalLeaderboard, "");
  			this.leaderboard = null;
  
  			this.leaderboardIndex = startingAmount - 1;
  
  			this.gameTimer = maxGameTime;
  			this.startTimer = -1;
  
  			spawnAll();
  		}
  	}
  
  	/**
  	 * Called when the arena starts.
  	 */
  	public void onStart() { }
  
  	/**
  	 * Arena Updater.
  	 */
  	public final void update()
  	{
  		if (stopped) // Don't tick if stopped
  			return;
  
  		checkTimers();
  		check();
  
  		for (ArenaPlayer ap : getActivePlayers())
  		{
  			Player player = ap.getPlayer();
  
  			// Make sure they're still online
  			if (! ap.isOnline())
  			{
  				ap.leaveArena(LeaveReason.QUIT);
  				continue;
  			}
  
  			// Kick them if they've reached the death limit and they're alive
  			if (ap.getDeaths() >= maxDeaths && player.getHealth() > 0.0D)
  			{
  				ap.leaveArena(LeaveReason.DEATHS);
  				continue;
  			}
  
  			// Lobby hats
  			if (isInLobby())
  				decideHat(ap);
  
  			// Class stuff
  			ArenaClass ac = ap.getArenaClass();
  			if (ac != null)
  			{
  				if (ac.getName().equalsIgnoreCase("healer"))
  				{
  					ap.putData("healTimer", ap.getDataInt("healTimer") - 1);
  					if (ap.getDataInt("healTimer") <= 0)
  					{
  						if (player.getHealth() > 0 && player.getHealth() < 20)
  						{
  							player.setHealth(Math.max(player.getHealth() + 1, 20));
  							ap.getData().put("healTimer", 2);
  						}
  					}
  				}
  
  				// Potion effects
  				if (! ap.isChangeClassOnRespawn() && ac.isHasPotionEffects())
  				{
  					if (ac.getPotionEffects().size() > 0)
  					{
  						for (PotionEffect effect : ac.getPotionEffects())
  						{
  							if (! player.hasPotionEffect(effect.getType()))
  								player.addPotionEffect(effect);
  						}
  					}
  				}
  			}
  
  			// Timer Stuff
  			if (! pauseStartTimer)
  			{
  				if (startTimer == 120 || startTimer == 60 || startTimer == 45 || startTimer == 30 || startTimer == 15)
  				{
  					ap.sendMessage(getMessage("secondsUntilStart"), startTimer);
  				}
  				if (startTimer > 0 && startTimer < 11)
  				{
  					ap.sendMessage(getMessage("secondsUntilStart"), startTimer);
  				}
  			}
  
  			if (gameTimer > 0 && gameTimer < 21)
  			{
  				ap.sendMessage(getMessage("secondsUntilEnd"), gameTimer);
  			}
  			if (gameTimer == 60 && maxGameTime > 60)
  			{
  				ap.sendMessage(getMessage("minutesUntilEnd"), gameTimer / 60);
  			}
  