Ver código fonte

Full improvement of the game-start process and adding item menus, Refactoring

RedstoneFuture 1 ano atrás
pai
commit
4ace541629
33 arquivos alterados com 2092 adições e 495 exclusões
  1. 15 10
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/MWCommands.java
  2. 58 34
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/UserCommands.java
  3. 98 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/ActionSet.java
  4. 68 22
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Config.java
  5. 12 6
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Messages.java
  6. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/arena/Arena.java
  7. 49 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/GameTeamConfiguration.java
  8. 10 13
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/Lobby.java
  9. 104 280
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java
  10. 269 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameJoinManager.java
  11. 138 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameLeaveManager.java
  12. 15 1
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/MapVoting.java
  13. 43 30
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Team.java
  14. 179 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/TeamManager.java
  15. 30 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/JoinIngameBehavior.java
  16. 31 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/RejoinIngameBehavior.java
  17. 5 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MissileWarsPlaceholder.java
  18. 3 5
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MotdManager.java
  19. 4 21
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/ScoreboardManager.java
  20. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/signs/MWSign.java
  21. 3 3
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/stats/FightStats.java
  22. 3 9
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java
  23. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/PlayerListener.java
  24. 57 6
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/EndListener.java
  25. 72 13
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/GameListener.java
  26. 68 36
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/LobbyListener.java
  27. 111 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/ItemRequirement.java
  28. 150 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuItem.java
  29. 55 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuUtils.java
  30. 63 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/hotbar/GameJoinMenu.java
  31. 201 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/MapVoteMenu.java
  32. 144 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/TeamSelectionMenu.java
  33. 28 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/player/MWPlayer.java

+ 15 - 10
missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/MWCommands.java

@@ -27,6 +27,7 @@ import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.game.Arenas;
 import de.butzlabben.missilewars.game.Game;
 import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.game.TeamManager;
 import de.butzlabben.missilewars.game.enums.GameResult;
 import de.butzlabben.missilewars.game.enums.GameState;
 import de.butzlabben.missilewars.game.enums.VoteState;
@@ -43,9 +44,9 @@ public class MWCommands extends BaseCommand {
     @Description("Shows information about the MissileWars Plugin.")
     public void mwCommand(CommandSender sender) {
 
-        sendHelpMessage(sender, "mw.vote", "/mw mapmenu", "Open the map-vote menu.");
+        sendHelpMessage(sender, "mw.mapmenu", "/mw mapmenu", "Open the map-vote menu.");
         sendHelpMessage(sender, "mw.vote", "/mw vote <arena>", "Vote for a arena.");
-        sendHelpMessage(sender, "mw.change.use", "/mw teammenu", "Open the team-change menu.");
+        sendHelpMessage(sender, "mw.teammenu", "/mw teammenu", "Open the team-change menu.");
         sendHelpMessage(sender, "mw.change.use", "/mw change <1|2|spec>", "Changes your team.");
         sendHelpMessage(sender, "mw.quit", "/mw quit", "Quit a game.");
 
@@ -81,16 +82,20 @@ public class MWCommands extends BaseCommand {
     public void listgamesCommand(CommandSender sender, String[] args) {
 
         sender.sendMessage(Messages.getPrefix() + "Current games:");
-
+        
         for (Game game : GameManager.getInstance().getGames().values()) {
+            TeamManager teamManager = game.getTeamManager();
+            
             sender.sendMessage("§e " + game.getLobby().getName() + "§7 -- Name: »" + game.getLobby().getDisplayName() + "§7« | Status: " + game.getState());
             sender.sendMessage("§8 - §f" + "Load with startup: §7" + game.getLobby().isAutoLoad());
             sender.sendMessage("§8 - §f" + "Current Arena: §7" + game.getArena().getName() + "§7 -- Name: »" + game.getArena().getDisplayName() + "§7«");
-            sender.sendMessage("§8 - §f" + "Total players: §7" + game.getPlayers().size() + "x");
-            sender.sendMessage("§8 - §f" + "Team 1: §7" + game.getTeam1().getColor() + game.getTeam1().getName()
-                    + " §7with " + game.getTeam1().getMembers().size() + " players");
-            sender.sendMessage("§8 - §f" + "Team 2: §7" + game.getTeam2().getColor() + game.getTeam2().getName()
-                    + " §7with " + game.getTeam2().getMembers().size() + " players");
+            sender.sendMessage("§8 - §f" + "Total players: §7" + game.getTotalGameUserAmount() + "x");
+            sender.sendMessage("§8 - §f" + "Team 1: §7" + teamManager.getTeam1().getColor() + teamManager.getTeam1().getName()
+                    + " §7with " + teamManager.getTeam1().getMembers().size() + " players");
+            sender.sendMessage("§8 - §f" + "Team 2: §7" + teamManager.getTeam2().getColor() + teamManager.getTeam2().getName()
+                    + " §7with " + teamManager.getTeam2().getMembers().size() + " players");
+            sender.sendMessage("§8 - §f" + "Spectators: §7" + teamManager.getTeamSpec().getColor() + teamManager.getTeamSpec().getName()
+                    + " §7with " + teamManager.getTeamSpec().getMembers().size() + " players");
         }
 
     }
@@ -208,8 +213,8 @@ public class MWCommands extends BaseCommand {
             }
         }
 
-        game.getTeam1().setGameResult(GameResult.DRAW);
-        game.getTeam2().setGameResult(GameResult.DRAW);
+        game.getTeamManager().getTeam1().setGameResult(GameResult.DRAW);
+        game.getTeamManager().getTeam2().setGameResult(GameResult.DRAW);
         if (game.getState() == GameState.INGAME) game.stopGame();
     }
 

+ 58 - 34
missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/UserCommands.java

@@ -30,7 +30,6 @@ import de.butzlabben.missilewars.game.GameManager;
 import de.butzlabben.missilewars.game.Team;
 import de.butzlabben.missilewars.game.enums.GameState;
 import de.butzlabben.missilewars.game.enums.MapChooseProcedure;
-import de.butzlabben.missilewars.game.enums.TeamType;
 import de.butzlabben.missilewars.player.MWPlayer;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
@@ -70,9 +69,9 @@ public class UserCommands extends BaseCommand {
         game.getMapVoting().addVote(player, args[0]);
     }
     
-    @Subcommand("mapmenu")
+    @Subcommand("mapmenu|votegui")
     @CommandCompletion("@nothing")
-    @CommandPermission("mw.vote")
+    @CommandPermission("mw.mapmenu")
     public void mapmenuCommand(CommandSender sender, String[] args) {
 
         if (!MWCommands.senderIsPlayer(sender)) return;
@@ -95,6 +94,12 @@ public class UserCommands extends BaseCommand {
         }
         
         // The GUI can also be opened when it is too late to vote according to the settings.
+        // But only in the LOBBY Game-State.
+        
+        if (game.getState() != GameState.LOBBY) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_CHANGE_TEAM_NO_LONGER_NOW));
+            return;
+        }
         
         MWPlayer mwPlayer = game.getPlayer(player);
         mwPlayer.getMapVoteMenu().openMenu();
@@ -124,50 +129,66 @@ public class UserCommands extends BaseCommand {
             return;
         }
         
-        // Is team change only in lobby allowed?
-        if (game.getState() != GameState.LOBBY) {
+        MWPlayer mwPlayer = game.getPlayer(player);
+        
+        if (game.getState() == GameState.LOBBY) {
+            if (game.getArena() != null) {
+                if (!game.getArena().isTeamchangeOngoingGame()) {
+                    // Is too late for team change (last seconds of lobby waiting-time)?
+                    if (game.getTaskManager().getTimer().getSeconds() < 10) {
+                        player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_CHANGE_TEAM_NO_LONGER_NOW));
+                        return;
+                    }
+                }
+            }
+            
+        } else if (game.getState() == GameState.INGAME) {
+            // Is team change only in lobby allowed?
             if (!game.getArena().isTeamchangeOngoingGame()) {
                 player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_CHANGE_TEAM_NOT_NOW));
                 return;
             }
             
-        // Is too late for team change (last seconds of lobby waiting-time)?
-        } else if (game.getArena() != null) {
-            if (!game.getArena().isTeamchangeOngoingGame()) {
-                if (game.getTaskManager().getTimer().getSeconds() < 10) {
-                    player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_CHANGE_TEAM_NO_LONGER_NOW));
-                    return;
-                }
+            // Anti-Spam Check:
+            if (mwPlayer.getWaitTimeForTeamChange() > 0) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_ANTISPAM_TEAM_CHANGE)
+                        .replace("%seconds%", Long.toString(mwPlayer.getWaitTimeForTeamChange())));
+                return;
             }
+            mwPlayer.setLastTeamChangeTime();
+            
+        } else {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_CHANGE_TEAM_NOT_NOW));
+            return;
+            
         }
         
-        MWPlayer mwPlayer = game.getPlayer(player);
         
         Team from = mwPlayer.getTeam();
         Team to;
         
         switch (args[0]) {
             case "1":
-                if (!player.hasPermission("mw.change.playerteam")) {
+                if (!player.hasPermission("mw.change.team.player")) {
                     Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
                     return;
                 }
-                to = game.getTeam1();
+                to = game.getTeamManager().getTeam1();
                 break;
             case "2":
-                if (!player.hasPermission("mw.change.playerteam")) {
+                if (!player.hasPermission("mw.change.team.player")) {
                     Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
                     return;
                 }
-                to = game.getTeam2();
+                to = game.getTeamManager().getTeam2();
                 break;
             case "spec":
             case "spectator":
-                if (!player.hasPermission("mw.change.spectator")) {
+                if (!player.hasPermission("mw.change.team.spectator")) {
                     Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
                     return;
                 }
-                to = game.getTeamSpec();
+                to = game.getTeamManager().getTeamSpec();
                 break;
             default:
                 sender.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_INVALID_TEAM));
@@ -179,31 +200,34 @@ public class UserCommands extends BaseCommand {
             player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_ALREADY_IN_TEAM));
             return;
         }
-
+        
         // Would the number of team members be too far apart?
-        if (!game.isValidFairSwitch(from, to)) {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_UNFAIR_TEAM_SIZE));
-            return;
+        if (game.getState() != GameState.LOBBY) {
+            if (!game.getTeamManager().isValidFairSwitch(from, to)) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_UNFAIR_TEAM_SIZE));
+                return;
+            }
         }
         
-        // Remove the player from the old team and add him to the new team
-        to.addMember(mwPlayer);
-        
-        if (to.getTeamType() == TeamType.SPECTATOR) {
-            if (game.getState() != GameState.LOBBY) {
-                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.ARENA_SPECTATOR));
+        // Checking max-user values:
+        if (to == game.getTeamManager().getTeamSpec()) {
+            if (game.areTooManySpectators()) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_MAX_REACHED));
+                return;
             }
         } else {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_TEAM_ASSIGNED).replace("%team%", to.getFullname()));
+            if (game.areTooManyPlayers()) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_MAX_REACHED));
+                return;
+            }
         }
         
-        game.getScoreboardManager().updateScoreboard();
-        mwPlayer.getGameJoinMenu().getMenu();
+        game.getGameJoinManager().runPlayerTeamSwitch(mwPlayer, to);
     }
     
-    @Subcommand("teammenu")
+    @Subcommand("teammenu|changegui")
     @CommandCompletion("@nothing")
-    @CommandPermission("mw.change.use")
+    @CommandPermission("mw.teammenu")
     public void teammenuCommand(CommandSender sender, String[] args) {
 
         if (!MWCommands.senderIsPlayer(sender)) return;

+ 98 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/ActionSet.java

@@ -0,0 +1,98 @@
+package de.butzlabben.missilewars.configuration;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Server;
+import org.bukkit.entity.Player;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ActionSet {
+    
+    @Getter
+    @RequiredArgsConstructor
+    public class Action {
+        
+        final ActionType actionType;
+        final String data;
+        
+    }
+    
+    public final List<Action> actionMap = new ArrayList<>();
+    
+    public ActionSet(List<String> actions) {
+        
+        actions.forEach(s -> {
+            String[] actionDef = s.split(" ", 2);
+            Action action = new Action(getActionType(actionDef[0]), actionDef[1]);
+            this.actionMap.add(action);
+        });
+        
+    }
+    
+    public enum ActionType {
+        PLAYER_CMD,
+        CONSOLE_CMD,
+        PLAYER_MSG,
+        GAME_MSG,
+        TEAM_MSG,
+        SERVER_MSG
+    }
+    
+    private ActionType getActionType(String actionDef) {
+        String prefix = actionDef.split(" ", 2)[0];
+        
+        // Action-Type specification inspired by DeluxeMenus https://wiki.helpch.at/helpchat-plugins/deluxemenus/options-and-configurations#actions-types
+        
+        switch (prefix) {
+            case "[player-cmd]": return ActionType.PLAYER_CMD;
+            case "[console-cmd]": return ActionType.CONSOLE_CMD;
+            case "[player-msg]": return ActionType.PLAYER_MSG;
+            case "[game-msg]": return ActionType.GAME_MSG;
+            case "[team-msg]": return ActionType.TEAM_MSG;
+            case "[server-msg]": return ActionType.SERVER_MSG;
+            default: return null;
+        }
+    }
+    
+    public void runActions(Player player, Game game) {
+        Server server = MissileWars.getInstance().getServer();
+        
+        actionMap.forEach(a -> {
+            
+            Logger.DEBUG.log("Run Action: " + a.getActionType() + " -> '" + a.data + "'");
+            
+            String data = Messages.getPapiMessage(a.getData(), player).replace("%prefix%", Messages.getPrefix());
+            switch (a.getActionType()) {
+                case PLAYER_CMD:
+                    player.performCommand(data);
+                    break;
+                case CONSOLE_CMD:
+                    server.dispatchCommand(server.getConsoleSender(), data);
+                    break;
+                case PLAYER_MSG:
+                    player.sendMessage(data);
+                    break;
+                case GAME_MSG:
+                    game.getPlayers().values().forEach(mwPlayer -> {
+                        mwPlayer.getPlayer().sendMessage(data);
+                    });
+                    break;
+                case TEAM_MSG:
+                    game.getPlayer(player).getTeam().getMembers().forEach(mwPlayer -> {
+                        mwPlayer.getPlayer().sendMessage(data);
+                    });
+                    break;
+                case SERVER_MSG:
+                    server.broadcastMessage(data);
+                    break;
+            }
+            
+        });
+        
+    }
+}

+ 68 - 22
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Config.java

@@ -72,6 +72,8 @@ public class Config {
         }
 
         cfg.addDefault("setup_mode", false);
+        
+        cfg.addDefault("antispam_intervall.team_change_command", 25);
 
         cfg.addDefault("contact_auth_server", true);
         cfg.addDefault("prefetch_players", true);
@@ -140,32 +142,57 @@ public class Config {
             cfg.addDefault(gameJoinMenu + ".items.team_selection.display_name", "&eTeam Selection");
             cfg.addDefault(gameJoinMenu + ".items.team_selection.material", "{player-team-name}");
             cfg.addDefault(gameJoinMenu + ".items.team_selection.slot", 2);
+            
+            cfg.addDefault(gameJoinMenu + ".items.team_selection.priority", 0);
 
             cfg.set(gameJoinMenu + ".items.team_selection.lore", new ArrayList<String>() {{
                 add("&2Right click to open the");
                 add("&2team selection menu!");
             }});
             
-            cfg.set(gameJoinMenu + ".items.team_selection.left_click_commands", new ArrayList<String>());
-            cfg.set(gameJoinMenu + ".items.team_selection.right_click_commands", new ArrayList<String>() {{
-                add("mw teammenu");
+            cfg.set(gameJoinMenu + ".items.team_selection.left_click_actions", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.team_selection.right_click_actions", new ArrayList<String>() {{
+                add("[player-cmd] mw teammenu");
             }});
             
             
-            // map-voting menu link:
+            // map-voting menu link (A: Map-Vote active):
             
-            cfg.addDefault(gameJoinMenu + ".items.mapVote.display_name", "&eMap Voting");
-            cfg.addDefault(gameJoinMenu + ".items.mapVote.material", "basehead-eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjFkZDRmZTRhNDI5YWJkNjY1ZGZkYjNlMjEzMjFkNmVmYTZhNmI1ZTdiOTU2ZGI5YzVkNTljOWVmYWIyNSJ9fX0=");
-            cfg.addDefault(gameJoinMenu + ".items.mapVote.slot", 4);
-
-            cfg.set(gameJoinMenu + ".items.mapVote.lore", new ArrayList<String>() {{
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.display_name", "&eMap Voting");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.material", "basehead-eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZmMyNzEwNTI3MTllZjY0MDc5ZWU4YzE0OTg5NTEyMzhhNzRkYWM0YzI3Yjk1NjQwZGI2ZmJkZGMyZDZiNWI2ZSJ9fX0=");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.slot", 4);
+
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.priority", 1);
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.view_requirement.type", "string equals");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.view_requirement.input", "%missilewars_lobby_mapvote_state_this%");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_active.view_requirement.output", "RUNNING");
+            
+            cfg.set(gameJoinMenu + ".items.mapVote_active.lore", new ArrayList<String>() {{
                 add("&2Right click to open the");
                 add("&2map vote menu!");
             }});
             
-            cfg.set(gameJoinMenu + ".items.mapVote.left_click_commands", new ArrayList<String>());
-            cfg.set(gameJoinMenu + ".items.mapVote.right_click_commands", new ArrayList<String>() {{
-                add("mw mapmenu");
+            cfg.set(gameJoinMenu + ".items.mapVote_active.left_click_actions", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.mapVote_active.right_click_actions", new ArrayList<String>() {{
+                add("[player-cmd] mw mapmenu");
+            }});
+            
+            
+            // map-voting menu link (B: Map-Vote inactive):
+            
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_inactive.display_name", "&2Map Voting");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_inactive.material", "basehead-eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjFkZDRmZTRhNDI5YWJkNjY1ZGZkYjNlMjEzMjFkNmVmYTZhNmI1ZTdiOTU2ZGI5YzVkNTljOWVmYWIyNSJ9fX0=");
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_inactive.slot", 4);
+
+            cfg.addDefault(gameJoinMenu + ".items.mapVote_inactive.priority", 0);
+            
+            cfg.set(gameJoinMenu + ".items.mapVote_inactive.lore", new ArrayList<String>() {{
+                add("&2Voted-Map: &7%missilewars_arena_displayname_this%");
+            }});
+            
+            cfg.set(gameJoinMenu + ".items.mapVote_inactive.left_click_actions", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.mapVote_inactive.right_click_actions", new ArrayList<String>() {{
+                add("[player-cmd] mw mapmenu");
             }});
             
             
@@ -174,6 +201,8 @@ public class Config {
             cfg.addDefault(gameJoinMenu + ".items.areaInfo.display_name", "&eArena Info");
             cfg.addDefault(gameJoinMenu + ".items.areaInfo.material", "basehead-eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjhlYTU3Yzc1NTFjNmFiMzNiOGZlZDM1NGI0M2RmNTIzZjFlMzU3YzRiNGY1NTExNDNjMzRkZGVhYzViNmM4ZCJ9fX0=");
             cfg.addDefault(gameJoinMenu + ".items.areaInfo.slot", 6);
+            
+            cfg.addDefault(gameJoinMenu + ".items.areaInfo.priority", 0);
 
             cfg.set(gameJoinMenu + ".items.areaInfo.lore", new ArrayList<String>() {{
                 add("&e> &fLobby: &7%missilewars_lobby_displayname_this%");
@@ -183,8 +212,8 @@ public class Config {
                 add("&e> &fArena-Size: &7%missilewars_lobby_arenasize_X_this% x %missilewars_lobby_arenasize_Z_this% blocks");
             }});
             
-            cfg.set(gameJoinMenu + ".items.areaInfo.left_click_commands", new ArrayList<String>());
-            cfg.set(gameJoinMenu + ".items.areaInfo.right_click_commands", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.areaInfo.left_click_actions", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.areaInfo.right_click_actions", new ArrayList<String>());
         }
         
         
@@ -225,6 +254,10 @@ public class Config {
     public static boolean isSetup() {
         return cfg.getBoolean("setup_mode");
     }
+    
+    public static int getTeamChangeCmdIntervall() {
+        return cfg.getInt("antispam_intervall.team_change_command");
+    }
 
     public static boolean isContactAuth() {
         return cfg.getBoolean("contact_auth_server");
@@ -371,23 +404,36 @@ public class Config {
         return Messages.getConvertedMsgList(cfg.getStringList("sidebar.entries"));
     }
     
-    public static Map<Integer, MenuItem> getGameJoinMenuItems() {
+    public static Map<Integer, Map<Integer, MenuItem>> getGameJoinMenuItems() {
+        // Config keys inspired by DeluxeMenus https://wiki.helpch.at/helpchat-plugins/deluxemenus/options-and-configurations/item
+        
         String gameJoinMenu = "menus.hotbar_menu.game_join_menu";
-        Map<Integer, MenuItem> menuItems = new HashMap<>();
         Set<String> items = Config.cfg.getConfigurationSection(gameJoinMenu + ".items").getKeys(false);
+
+        Map<Integer, Map<Integer, MenuItem>> menuItems = new HashMap<>();
         
         for (String item : items) {
             ConfigurationSection cfg = Config.cfg.getConfigurationSection(gameJoinMenu + ".items." + item);
-            MenuItem menuItem = new MenuItem();
-            
+            MenuItem menuItem = new MenuItem(cfg.getInt("slot"), cfg.getInt("priority"));
+
             menuItem.setDisplayName(Messages.getConvertedMsg(cfg.getString("display_name")));
             menuItem.setMaterialName(cfg.getString("material"));
-            menuItem.setSlot(cfg.getInt("slot"));
+            menuItem.setItemRequirement(cfg);
             menuItem.setLoreList(Messages.getConvertedMsgList(cfg.getStringList("lore")));
-            menuItem.setLeftClickCmdList(Messages.getConvertedMsgList(cfg.getStringList("left_click_commands")));
-            menuItem.setRightClickCmdList(Messages.getConvertedMsgList(cfg.getStringList("right_click_commands")));
+            menuItem.setLeftClickActions(new ActionSet(Messages.getConvertedMsgList(cfg.getStringList("left_click_actions"))));
+            menuItem.setRightClickActions(new ActionSet(Messages.getConvertedMsgList(cfg.getStringList("right_click_actions"))));
+            
+            int slot = menuItem.getSlot();
+            Map<Integer, MenuItem> itemsInSlot = new HashMap<>();
             
-            menuItems.put(menuItem.getSlot(), menuItem);
+            if (!menuItems.containsKey(slot)) {
+                itemsInSlot.put(menuItem.getPriority(), menuItem);
+                menuItems.put(slot, itemsInSlot);
+            } else {
+                itemsInSlot = menuItems.get(slot);
+                itemsInSlot.put(menuItem.getPriority(), menuItem);
+                menuItems.replace(slot, itemsInSlot);
+            }
         }
         
         return menuItems;

+ 12 - 6
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Messages.java

@@ -111,6 +111,7 @@ public class Messages {
         COMMAND_INVALID_GAME("command.invalid_game", "&cThe specified game %input% was not found."),
         COMMAND_INVALID_MAP("command.invalid_map", "&cThe specified map %input% was not found."),
         COMMAND_INVALID_TEAM("command.invalid_team", "&cThe team selection in invalid. Use \"1\" or \"2\" to join on of the player-teams or use \"spec\" to enter the game as spectator."),
+        COMMAND_ANTISPAM_TEAM_CHANGE("command.antispam.team_change", "&cYou have to wait %seconds% seconds before you can change teams again."),
         
         GAME_PLAYER_JOINED("game.player_joined", "&e%player% &7joined the game (%team%&7)."),
         GAME_PLAYER_LEFT("game.player_left", "&e%player% &7left the game (%team%&7)."),
@@ -119,7 +120,10 @@ public class Messages {
         GAME_NOT_ENTER_ARENA("game.not_enter_arena", "&cYou may not enter this arena right now."),
         GAME_ALREADY_STARTET("game.already_startet", "&cGame already started."),
         GAME_CAN_NOT_STARTET("game.can_not_startet", "&cGame cannot be started."),
-
+        GAME_GAME_STARTS("game.game_starts", "&aThe game starts."),
+        GAME_MAX_REACHED("game.max_reached", "&cUnfortunately, the lobby is full. You can no longer enter it. Please look for another lobby or wait for the next round."),
+        GAME_REJOINED("game.rejoined", "&eWelcome back! &7A rejoin to the old team is being considered ..."),
+        
         LOBBY_TIMER_GAME_STARTS_IN("lobby_timer.game_starts_in", "&7Game starts in &e%seconds% &7seconds."),
 
         GAME_TIMER_GAME_ENDS_IN_MINUTES("game_timer.game_ends_in_minutes", "&7Game ends in &e%minutes% &7minutes."),
@@ -132,20 +136,22 @@ public class Messages {
         LOBBY_LEFT("lobby.left", "&7You left the MissileWars lobby."),
         LOBBY_NOT_ENOUGH_PLAYERS("lobby.not_enough_players", "&cThere are not enough players online."),
         LOBBY_TEAMS_UNEQUAL("lobby.teams_unequal", "&cThe teams are unequal distributed."),
-        LOBBY_GAME_STARTS("lobby.game_starts", "&aThe game starts."),
-
+        
         TEAM_CHANGE_TEAM_NOT_NOW("team.change_team_not_now", "&cThe game is not in the right state to change your team right now."),
         TEAM_CHANGE_TEAM_NO_LONGER_NOW("team.change_team_no_longer_now", "&cNow you cannot change your team anymore."),
         TEAM_ALREADY_IN_TEAM("team.already_in_team", "&cYou are already in this team."),
         TEAM_UNFAIR_TEAM_SIZE("team.unfair_team_size", "&cChanging the team would make the number of team members more uneven."),
-        TEAM_TEAM_CHANGED("team.team_changed", "&7You are now in %team%&7."),
-        TEAM_TEAM_ASSIGNED("team.team_assigned", "&7You have been assigned to %team%&7."),
+        TEAM_PLAYER_TEAM_CHANGED("team.player.team_changed", "&7You are now in %team%&7."),
+        TEAM_SPECTATOR_TEAM_CHANGED("team.spectator.team_changed", "&7You are now a %team%&7."),
+        TEAM_PLAYER_TEAM_ASSIGNED("team.player.team_assigned", "&7You have been assigned to %team%&7."),
+        TEAM_SPECTATOR_TEAM_ASSIGNED("team.spectator.team_assigned", "&7You have been assigned to spectator."),
+        TEAM_PLAYER_MAX_REACHED("team.player.max_reached", "&cThe maximum number of players has been reached."),
+        TEAM_SPECTATOR_MAX_REACHED("team.spectator.max_reached", "&cThe maximum number of spectators has been reached."),
         TEAM_ALL_TEAMMATES_OFFLINE("team.all_teammates_offline", "&7Everyone from %team% &7is offline."),
         TEAM_TEAM_BUFFED("team.team_buffed", "%team% &7was buffed as one player left the team."),
         TEAM_TEAM_NERVED("team.team_nerved", "%team% &7was nerved as one player joined the team."),
         TEAM_HURT_TEAMMATES("team.hurt_teammates", "&cYou must not hurt your teammates."),
 
-        ARENA_SPECTATOR("arena.spectator", "&7You are now a spectator."),
         ARENA_ARENA_LEAVE("arena.arena_leave", "&cYou are not allowed to leave the arena."),
         ARENA_MISSILE_PLACE_DENY("arena.missile_place_deny", "&cYou are not allowed to place a missile here."),
         ARENA_NOT_HIGHER("arena.not_higher", "&cYou can not go higher."),

+ 2 - 2
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/arena/Arena.java

@@ -45,7 +45,6 @@ public class Arena implements Cloneable {
     @SerializedName("keep_inventory") private boolean keepInventory = false;
     @SerializedName("max_height") private int maxHeight = 170;
     @SerializedName("death_height") private int deathHeight = 65;
-    @SerializedName("max_spectators") private int maxSpectators = -1;
     @SerializedName("game_duration") private int gameDuration = 30;
     @SerializedName("fireball") private FireballConfiguration fireballConfiguration = new FireballConfiguration();
     @SerializedName("arrow") private ArrowConfiguration arrowConfiguration = new ArrowConfiguration();
@@ -56,6 +55,7 @@ public class Arena implements Cloneable {
     @SerializedName("missile") private MissileConfiguration missileConfiguration = new MissileConfiguration();
     @SerializedName("shield") private ShieldConfiguration shieldConfiguration = new ShieldConfiguration();
     @Setter @SerializedName("area") private AreaConfiguration areaConfig = new AreaConfiguration(-30, 0, -72, 30, 256, 72);
+    @SerializedName("teamchange_ongoing_game") private boolean teamchangeOngoingGame = false;
 
     @SerializedName("spectator_spawn")
     @Setter
@@ -76,7 +76,7 @@ public class Arena implements Cloneable {
     public Arena() {
 
     }
-
+    
     @Override
     public Arena clone() {
         try {

+ 49 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/GameTeamConfiguration.java

@@ -0,0 +1,49 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.configuration.lobby;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.bukkit.configuration.serialization.ConfigurationSerializable;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+@ToString
+@AllArgsConstructor
+public class GameTeamConfiguration implements ConfigurationSerializable {
+
+    private String name;
+    private String color;
+    
+    /**
+     * This method is used to save the config entries in the config file.
+     */
+    @Override
+    @NotNull
+    public Map<String, Object> serialize() {
+        Map<String, Object> serialized = new HashMap<>();
+        serialized.put("name", name);
+        serialized.put("color", color);
+        return serialized;
+    }
+}

+ 10 - 13
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/Lobby.java

@@ -23,15 +23,11 @@ import de.butzlabben.missilewars.Logger;
 import de.butzlabben.missilewars.configuration.arena.AreaConfiguration;
 import de.butzlabben.missilewars.configuration.arena.Arena;
 import de.butzlabben.missilewars.game.Arenas;
+import de.butzlabben.missilewars.game.enums.JoinIngameBehavior;
 import de.butzlabben.missilewars.game.enums.MapChooseProcedure;
+import de.butzlabben.missilewars.game.enums.RejoinIngameBehavior;
 import de.butzlabben.missilewars.util.geometry.GameArea;
 import de.butzlabben.missilewars.util.serialization.Serializer;
-import java.io.File;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import lombok.Setter;
@@ -57,17 +53,18 @@ public class Lobby {
     @SerializedName("auto_load") private boolean autoLoad = true;
     @SerializedName("world") private String worldName = getBukkitDefaultWorld().getName();
     @SerializedName("lobby_time") private int lobbyTime = 60;
-    @SerializedName("join_ongoing_game") private boolean joinOngoingGame = false;
-    @SerializedName("min_size") private int minSize = 2;
-    @SerializedName("max_size") private int maxSize = 20;
-    @SerializedName("team1_name") private String team1Name = "Team1";
-    @SerializedName("team1_color") private String team1Color = "&c";
-    @SerializedName("team2_name") private String team2Name = "Team2";
-    @SerializedName("team2_color") private String team2Color = "&a";
+    @SerializedName("min_players") private int minPlayers = 2;
+    @SerializedName("max_players") private int maxPlayers = 20;
+    @SerializedName("max_spectators") private int maxSpectators = -1;
+    @SerializedName("team_1") private GameTeamConfiguration team1Config = new GameTeamConfiguration("Team1", "&c");
+    @SerializedName("team_2") private GameTeamConfiguration team2Config = new GameTeamConfiguration("Team2", "&a");
+    @SerializedName("team_spectator") private GameTeamConfiguration teamConfigSpec = new GameTeamConfiguration("Spectator", "&f");
     @Setter @SerializedName("spawn_point") private Location spawnPoint = getBukkitDefaultWorld().getSpawnLocation();
     @Setter @SerializedName("after_game_spawn") private Location afterGameSpawn = getBukkitDefaultWorld().getSpawnLocation();
     @Setter @SerializedName("area") private AreaConfiguration areaConfig = AreaConfiguration.aroundLocation(getBukkitDefaultWorld().getSpawnLocation(), 30);
     @SerializedName("map_choose_procedure") private MapChooseProcedure mapChooseProcedure = MapChooseProcedure.FIRST;
+    @SerializedName("join_ongoing_game") private JoinIngameBehavior joinIngameBehavior = JoinIngameBehavior.SPECTATOR;
+    @SerializedName("rejoin_ongoing_game") private RejoinIngameBehavior rejoinIngameBehavior = RejoinIngameBehavior.LAST_TEAM;
     @SerializedName("possible_arenas") private List<String> possibleArenas = new ArrayList<>() {{
         add("arena0");
     }};

+ 104 - 280
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java

@@ -29,7 +29,7 @@ import de.butzlabben.missilewars.event.GameStopEvent;
 import de.butzlabben.missilewars.game.enums.GameResult;
 import de.butzlabben.missilewars.game.enums.GameState;
 import de.butzlabben.missilewars.game.enums.MapChooseProcedure;
-import de.butzlabben.missilewars.game.enums.VoteState;
+import de.butzlabben.missilewars.game.enums.TeamType;
 import de.butzlabben.missilewars.game.equipment.EquipmentManager;
 import de.butzlabben.missilewars.game.misc.MotdManager;
 import de.butzlabben.missilewars.game.misc.ScoreboardManager;
@@ -42,13 +42,11 @@ import de.butzlabben.missilewars.game.timer.EndTimer;
 import de.butzlabben.missilewars.game.timer.GameTimer;
 import de.butzlabben.missilewars.game.timer.LobbyTimer;
 import de.butzlabben.missilewars.game.timer.TaskManager;
-import de.butzlabben.missilewars.inventory.OrcItem;
 import de.butzlabben.missilewars.listener.game.EndListener;
 import de.butzlabben.missilewars.listener.game.GameBoundListener;
 import de.butzlabben.missilewars.listener.game.GameListener;
 import de.butzlabben.missilewars.listener.game.LobbyListener;
 import de.butzlabben.missilewars.player.MWPlayer;
-import de.butzlabben.missilewars.util.PlayerDataProvider;
 import de.butzlabben.missilewars.util.geometry.GameArea;
 import de.butzlabben.missilewars.util.geometry.Geometry;
 import de.butzlabben.missilewars.util.serialization.Serializer;
@@ -59,16 +57,12 @@ import org.bukkit.entity.Fireball;
 import org.bukkit.entity.Player;
 import org.bukkit.entity.Snowball;
 import org.bukkit.event.HandlerList;
-import org.bukkit.event.player.PlayerTeleportEvent;
 import org.bukkit.inventory.ItemStack;
 import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.scheduler.BukkitTask;
 import org.bukkit.util.Vector;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.function.Consumer;
 
 /**
@@ -77,7 +71,7 @@ import java.util.function.Consumer;
  */
 
 @Getter
-@ToString(of = {"gameWorld", "players", "lobby", "arena", "team1", "team2", "state"})
+@ToString(of = {"gameWorld", "players", "lobby", "arena", "state"})
 public class Game {
 
     private static final Map<String, Integer> cycles = new HashMap<>();
@@ -87,8 +81,7 @@ public class Game {
     private final Lobby lobby;
     private final Map<UUID, BukkitTask> playerTasks = new HashMap<>();
     private GameState state = GameState.LOBBY;
-    private Team team1;
-    private Team team2;
+    private TeamManager teamManager;
     private boolean ready = false;
     private boolean restart = false;
     private GameWorld gameWorld;
@@ -97,11 +90,13 @@ public class Game {
     private long timestart;
     private Arena arena;
     private ScoreboardManager scoreboardManager;
+    private GameJoinManager gameJoinManager;
+    private GameLeaveManager gameLeaveManager;
     private GameBoundListener listener;
     private EquipmentManager equipmentManager;
     private TaskManager taskManager;
     private int remainingGameDuration;
-
+    
     public Game(Lobby lobby) {
         Logger.BOOT.log("Loading lobby \"" + lobby.getName() + "\".");
         this.lobby = lobby;
@@ -128,13 +123,9 @@ public class Game {
             Logger.ERROR.log("None of the specified arenas match a real arena for the lobby \"" + lobby.getName() + "\".");
             return;
         }
-
-        team1 = new Team(lobby.getTeam1Name(), lobby.getTeam1Color(), this);
-        team2 = new Team(lobby.getTeam2Name(), lobby.getTeam2Color(), this);
-
-        team1.createTeamArmor();
-        team2.createTeamArmor();
-
+        
+        teamManager = new TeamManager(this);
+        
         Logger.DEBUG.log("Registering, teleporting, etc. all players");
 
         updateMOTD();
@@ -148,7 +139,7 @@ public class Game {
         taskManager.runTimer(0, 20);
         state = GameState.LOBBY;
 
-        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> applyForAllPlayers(this::runTeleportEventForPlayer), 2);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> applyForAllPlayers(player -> gameJoinManager.runTeleportEventForPlayer(player)), 2);
 
         if (Config.isSetup()) {
             Logger.WARN.log("Did not fully initialize lobby \"" + lobby.getName() + "\" as the plugin is in setup mode");
@@ -156,7 +147,9 @@ public class Game {
         }
 
         scoreboardManager = new ScoreboardManager(this);
-
+        gameJoinManager = new GameJoinManager(this);
+        gameLeaveManager = new GameLeaveManager(this);
+        
         // choose the game arena
         if (lobby.getMapChooseProcedure() == MapChooseProcedure.FIRST) {
             setArena(lobby.getArenas().get(0));
@@ -177,7 +170,7 @@ public class Game {
                 prepareGame();
             } else {
                 mapVoting.startVote();
-                scoreboardManager.resetScoreboard();
+                updateGameInfo();
             }
         }
 
@@ -193,11 +186,8 @@ public class Game {
         if (this.arena == null) {
             throw new IllegalStateException("The arena is not yet set");
         }
-
-        // Clear the player inventory
-        applyForAllPlayers(player -> player.getInventory().setItem(4, new ItemStack(Material.AIR)));
-
-        scoreboardManager.resetScoreboard();
+        
+        updateGameInfo();
 
         equipmentManager = new EquipmentManager(this);
         equipmentManager.createGameItems();
@@ -252,7 +242,7 @@ public class Game {
 
         timestart = System.currentTimeMillis();
 
-        applyForAllPlayers(this::startForPlayer);
+        applyForAllPlayers(player -> gameJoinManager.startForPlayer(player, true));
 
         updateMOTD();
 
@@ -318,158 +308,9 @@ public class Game {
             teleportToFallbackSpawn(mwPlayer.getPlayer());
         }
 
-        gameWorld.unload();
-    }
-
-    /**
-     * This method adds the player to the game.
-     *
-     * @param player          the target Player
-     * @param isSpectatorJoin should the player join as spectator or as normal player
-     */
-    public void playerJoinInGame(Player player, boolean isSpectatorJoin) {
-
-        PlayerDataProvider.getInstance().storeInventory(player);
-        MWPlayer mwPlayer = addPlayer(player);
-
-        if (state == GameState.LOBBY) {
-            assert !isSpectatorJoin : "wrong syntax";
-
-            teleportToLobbySpawn(player);
-            player.setGameMode(GameMode.ADVENTURE);
-        }
-
-        if (isSpectatorJoin) {
-            Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> teleportToArenaSpectatorSpawn(player), 2);
-            Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> player.setGameMode(GameMode.SPECTATOR), 35);
-
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.ARENA_SPECTATOR));
-            player.setDisplayName("§7" + player.getName() + "§r");
-
-        } else {
-            player.getInventory().clear();
-            player.setFoodLevel(20);
-            player.setHealth(player.getMaxHealth());
-
-            Team team = getSmallerTeam();
-            team.addMember(mwPlayer);
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_TEAM_ASSIGNED).replace("%team%", team.getFullname()));
-
-            String message = null;
-            if (state == GameState.LOBBY) {
-                message = Messages.getMessage(true, Messages.MessageEnum.LOBBY_PLAYER_JOINED);
-            } else if (state == GameState.INGAME) {
-                message = Messages.getMessage(true, Messages.MessageEnum.GAME_PLAYER_JOINED);
-            }
-
-            if (message != null) {
-                broadcast(message.replace("%max_players%", Integer.toString(lobby.getMaxSize()))
-                        .replace("%players%", Integer.toString(players.values().size()))
-                        .replace("%player%", player.getName())
-                        .replace("%team%", team.getFullname()));
-            }
-
-        }
-
-        player.setScoreboard(scoreboardManager.getBoard());
-
-        if (state == GameState.LOBBY) {
-
-            // team change menu:
-            if (player.hasPermission("mw.change")) {
-                player.getInventory().setItem(0, team1.getGlassPlane());
-                player.getInventory().setItem(8, team2.getGlassPlane());
-            }
-
-            // map choose menu:
-            if (mapVoting.getState() == VoteState.RUNNING) {
-                if (player.hasPermission("mw.vote")) {
-                    player.getInventory().setItem(4, new OrcItem(Material.NETHER_STAR, "§3Vote Map").getItemStack());
-                }
-            }
-
-        } else if ((state == GameState.INGAME) && (!isSpectatorJoin)) {
-            startForPlayer(player);
-        }
-    }
-
-    /**
-     * This method handles the removal of the player from the game.
-     *
-     * @param mwPlayer the target missilewars player
-     */
-    public void playerLeaveFromGame(MWPlayer mwPlayer) {
-        Player player = mwPlayer.getPlayer();
-        Team team = mwPlayer.getTeam();
-        boolean playerWasTeamMember = false;
-
-        if (state == GameState.INGAME) {
-            BukkitTask task = playerTasks.get(mwPlayer.getUuid());
-            if (task != null) task.cancel();
-        }
-
-        PlayerDataProvider.getInstance().loadInventory(player);
-
-        if (team != null) {
-            playerWasTeamMember = true;
-            team.removeMember(mwPlayer);
-            if (state == GameState.INGAME) checkTeamSize(team);
-        }
-
-        removePlayer(mwPlayer);
-
-        if (playerWasTeamMember) {
-
-            String message = null;
-            if (state == GameState.LOBBY) {
-                message = Messages.getMessage(true, Messages.MessageEnum.LOBBY_PLAYER_LEFT);
-            } else if (state == GameState.INGAME) {
-                message = Messages.getMessage(true, Messages.MessageEnum.GAME_PLAYER_LEFT);
-            }
-
-            if (message != null) {
-                broadcast(message.replace("%max_players%", Integer.toString(lobby.getMaxSize()))
-                        .replace("%players%", Integer.toString(players.values().size()))
-                        .replace("%player%", player.getName())
-                        .replace("%team%", team.getFullname()));
-            }
-
-        }
-
-        player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
-
-        if (state == GameState.LOBBY) {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.LOBBY_LEFT).replace("%lobby_name%", lobby.getDisplayName()));
-        } else if (state == GameState.INGAME) {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_LEFT).replace("%arena_name%", arena.getDisplayName()));
-        }
-
+        if (gameWorld != null) gameWorld.unload();
     }
-
-    /**
-     * This method executes the PlayerTeleportEvent to run the basic game join process
-     * after the game is restarted.
-     *
-     * @param player target player
-     */
-    private void runTeleportEventForPlayer(Player player) {
-        Bukkit.getPluginManager().callEvent(new PlayerTeleportEvent(player,
-                Config.getFallbackSpawn(), lobby.getSpawnPoint()));
-    }
-
-    private void checkTeamSize(Team team) {
-        int teamSize = team.getMembers().size();
-        if (teamSize == 0) {
-            Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> {
-                team.getEnemyTeam().setGameResult(GameResult.WIN);
-                team.setGameResult(GameResult.LOSE);
-                sendGameResult();
-                stopGame();
-            });
-            broadcast(Messages.getMessage(true, Messages.MessageEnum.TEAM_ALL_TEAMMATES_OFFLINE).replace("%team%", team.getFullname()));
-        }
-    }
-
+    
     public void resetGame() {
         // Teleporting players; the event listener will handle the teleport event
         applyForAllPlayers(this::teleportToAfterGameSpawn);
@@ -548,14 +389,7 @@ public class Game {
     public boolean isIn(Location location) {
         return isInLobbyArea(location) || isInGameWorld(location);
     }
-
-    private MWPlayer addPlayer(Player player) {
-        if (players.containsKey(player.getUniqueId())) return players.get(player.getUniqueId());
-        MWPlayer mwPlayer = new MWPlayer(player, this);
-        players.put(player.getUniqueId(), mwPlayer);
-        return mwPlayer;
-    }
-
+    
     public MWPlayer getPlayer(Player player) {
         return players.get(player.getUniqueId());
     }
@@ -564,7 +398,7 @@ public class Game {
      * This method finally removes the player from the game player array. Besides former
      * team members, it also affects spectators.
      */
-    private void removePlayer(MWPlayer mwPlayer) {
+    public void removePlayer(MWPlayer mwPlayer) {
         players.remove(mwPlayer.getUuid());
     }
 
@@ -575,24 +409,6 @@ public class Game {
         }
     }
 
-    public void startForPlayer(Player player) {
-        MWPlayer mwPlayer = getPlayer(player);
-        if (mwPlayer == null) {
-            Logger.ERROR.log("Error starting game at player " + player.getName());
-            return;
-        }
-
-        player.teleport(mwPlayer.getTeam().getSpawn());
-
-        equipmentManager.sendGameItems(player, false);
-        setPlayerAttributes(player);
-
-        mwPlayer.setRandomGameEquipment(new PlayerEquipmentRandomizer(mwPlayer, this));
-
-        playerTasks.put(player.getUniqueId(),
-                Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), mwPlayer, 40, 20));
-    }
-
     /**
      * This method sets the player attributes (game mode, level, enchantments, ...).
      *
@@ -701,8 +517,9 @@ public class Game {
 
         try {
             Serializer.setWorldAtAllLocations(this.arena, gameWorld.getWorld());
-            team1.setSpawn(this.arena.getTeam1Spawn());
-            team2.setSpawn(this.arena.getTeam2Spawn());
+            teamManager.getTeam1().setSpawn(this.arena.getTeam1Spawn());
+            teamManager.getTeam2().setSpawn(this.arena.getTeam2Spawn());
+            teamManager.getTeamSpec().setSpawn(this.arena.getSpectatorSpawn());
         } catch (Exception exception) {
             Logger.ERROR.log("Could not inject world object at arena " + this.arena.getName());
             exception.printStackTrace();
@@ -729,16 +546,16 @@ public class Game {
             x1 = gameArea.getMinX();
             x2 = gameArea.getMaxX();
 
-            z1 = team1.getSpawn().getBlockZ();
-            z2 = team2.getSpawn().getBlockZ();
+            z1 = teamManager.getTeam1().getSpawn().getBlockZ();
+            z2 = teamManager.getTeam2().getSpawn().getBlockZ();
 
         } else {
 
             z1 = gameArea.getMinZ();
             z2 = gameArea.getMaxZ();
 
-            x1 = team1.getSpawn().getBlockX();
-            x2 = team2.getSpawn().getBlockX();
+            x1 = teamManager.getTeam1().getSpawn().getBlockX();
+            x2 = teamManager.getTeam2().getSpawn().getBlockX();
 
         }
 
@@ -764,28 +581,16 @@ public class Game {
 
         for (Player player : gameWorld.getWorld().getPlayers()) {
             MWPlayer mwPlayer = getPlayer(player);
-
-            // team member of team 1
-            if (team1.isMember(mwPlayer)) {
-                team1.sendMoney(mwPlayer);
-                team1.sendGameResultTitle(mwPlayer);
-                team1.sendGameResultSound(mwPlayer);
-                continue;
-            }
-
-            // team member of team 2
-            if (team2.isMember(mwPlayer)) {
-                team2.sendMoney(mwPlayer);
-                team2.sendGameResultTitle(mwPlayer);
-                team2.sendGameResultSound(mwPlayer);
-                continue;
-            }
-
-            // spectator
-            if (player.isOnline()) {
+            Team team = mwPlayer.getTeam();
+            
+            if (team.getTeamType() == TeamType.PLAYER) {
+                team.sendMoney(mwPlayer);
+                team.sendGameResultTitle(mwPlayer);
+                team.sendGameResultSound(mwPlayer);
+            } else {
                 sendNeutralGameResultTitle(player);
             }
-
+            
         }
     }
 
@@ -797,12 +602,14 @@ public class Game {
         String title;
         String subTitle;
 
-        if (team1.getGameResult() == GameResult.WIN) {
-            title = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_TITLE_WON).replace("%team%", team1.getName());
+        if (teamManager.getTeam1().getGameResult() == GameResult.WIN) {
+            title = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_TITLE_WON)
+                    .replace("%team%", teamManager.getTeam1().getName());
             subTitle = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_SUBTITLE_WON);
 
-        } else if (team2.getGameResult() == GameResult.WIN) {
-            title = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_TITLE_WON).replace("%team%", team2.getName());
+        } else if (teamManager.getTeam2().getGameResult() == GameResult.WIN) {
+            title = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_TITLE_WON)
+                    .replace("%team%", teamManager.getTeam2().getName());
             subTitle = Messages.getMessage(false, Messages.MessageEnum.GAME_RESULT_SUBTITLE_WON);
 
         } else {
@@ -820,62 +627,79 @@ public class Game {
     public void updateGameInfo() {
         MissileWars.getInstance().getSignRepository().getSigns(this).forEach(MWSign::update);
         scoreboardManager.resetScoreboard();
-        Logger.DEBUG.log("Updated signs and scoreboard.");
+        if (state == GameState.LOBBY) players.forEach((uuid, mwPlayer) -> mwPlayer.getGameJoinMenu().getMenu());
+        
+        Logger.DEBUG.log("Updated signs, scoreboard and menus.");
     }
-
+    
     /**
-     * This method returns the next matching team for the next player to
-     * join. It is always the smaller team.
-     *
-     * @return (Team) the smaller team
+     * This method checks whether there are too few players in 
+     * the lobby based of the configuration. (Spectators are not 
+     * counted here!)
+     * 
+     * @return (boolean) 'true' if to few players are in the lobby 
      */
-    public Team getSmallerTeam() {
-        if (team1.getMembers().size() > team2.getMembers().size()) {
-            return team2;
-        } else {
-            return team1;
-        }
+    public boolean areToFewPlayers() {
+        int minSize = lobby.getMinPlayers();
+        int currentSize = teamManager.getTeam1().getMembers().size() + teamManager.getTeam2().getMembers().size();
+        return currentSize < minSize;
     }
 
     /**
-     * This method checks whether a team switch would be fair based on 
-     * the new team size. If no empty team results or if the team size 
-     * difference does not exceed a certain value, the switch is 
-     * considered acceptable.
+     * This method checks whether there are too many players in 
+     * the lobby based of the configuration. (Spectators are not 
+     * counted here!)
      * 
-     * @param targetTeam the new team
-     * @return (boolean) 'true' if it's a fair team switch
+     * @return (boolean) 'true' if to many players are in the lobby 
      */
-    public boolean isValidTeamSwitch(Team targetTeam) {
+    public boolean areTooManyPlayers() {
+        int maxSize = lobby.getMaxPlayers();
         
-        // original team sizes
-        int targetTeamSize = targetTeam.getMembers().size();
-        int currentTeamSize = targetTeam.getEnemyTeam().getMembers().size();
+        if (maxSize == -1) return false;
         
-        // Preventing an empty team when previously both teams had at least one player:
-        if ((currentTeamSize == 1) && (targetTeamSize >= 1)) return false;
-
-        int diff = getSmallerTeam().getEnemyTeam().getMembers().size() - getSmallerTeam().getMembers().size();
-
-        // max team difference: 30% (rounded) of target team size
-        float maxDiff = Math.max(1, Math.round(targetTeamSize * 0.3));
-
-        return diff <= maxDiff;
+        return getPlayerAmount() > maxSize;
     }
-
-    public boolean isPlayersMax() {
-        int maxSize = lobby.getMaxSize();
-        int currentSize = team1.getMembers().size() + team2.getMembers().size();
-        return currentSize >= maxSize;
-    }
-
-    public boolean isSpectatorsMax() {
-        int maxSize = arena.getMaxSpectators();
-
+    
+    /**
+     * This method checks whether there are too many spectators in 
+     * the lobby based of the configuration.
+     * 
+     * @return (boolean) 'true' if to many spectators are in the lobby 
+     */
+    public boolean areTooManySpectators() {
+        int maxSize = lobby.getMaxSpectators();
+        
         if (maxSize == -1) return false;
-
-        int currentSize = players.size() - (team1.getMembers().size() + team2.getMembers().size());
-        return currentSize >= maxSize;
+        
+        int currentSize = teamManager.getTeamSpec().getMembers().size();
+        return currentSize > maxSize;
+    }
+    
+    public int getGameDuration() {
+        int time = 0;
+        if (arena == null) return time;
+        
+        if (state == GameState.LOBBY) {
+            // Show the planned duration of the next game:
+            time = arena.getGameDuration();
+        } else if (state == GameState.INGAME) {
+            // Show the remaining duration of the running game:
+            time = getTaskManager().getTimer().getSeconds() / 60;
+        } else if (state == GameState.END) {
+            // Show the remaining duration of the last game:
+            time = getRemainingGameDuration() / 60;
+        }
+        
+        return time;
+    }
+    
+    public int getTotalGameUserAmount() {
+        return players.size();
+    }
+    
+    public int getPlayerAmount() {
+        if ((teamManager.getTeam1() == null) || (teamManager.getTeam2() == null)) return 0;
+        return teamManager.getTeam1().getMembers().size() + teamManager.getTeam2().getMembers().size();
     }
 
     public static void knockbackEffect(Player player, Location from, Location to) {

+ 269 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameJoinManager.java

@@ -0,0 +1,269 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.configuration.Config;
+import de.butzlabben.missilewars.configuration.Messages;
+import de.butzlabben.missilewars.game.enums.GameState;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import de.butzlabben.missilewars.menus.hotbar.GameJoinMenu;
+import de.butzlabben.missilewars.player.MWPlayer;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Bukkit;
+import org.bukkit.GameMode;
+import org.bukkit.Sound;
+import org.bukkit.entity.Player;
+import org.bukkit.event.player.PlayerTeleportEvent;
+
+import java.util.HashMap;
+
+@RequiredArgsConstructor
+public class GameJoinManager {
+    
+    private final Game game;
+    private final TeamManager teamManager;
+    
+    public GameJoinManager(Game game) {
+        this.game = game;
+        this.teamManager = game.getTeamManager();
+        
+        GameJoinMenu.setMenuItems(Config.getGameJoinMenuItems());
+    }
+    
+    /**
+     * This method adds the player to the game.
+     *
+     * @param player (Player) the target Player
+     * @param targetTeamType (TeamType) Should the player join in a "player-team" (Team1, Team2) or in the "spectator-team" (Spectator)?
+     */
+    public void runPlayerJoin(Player player, TeamType targetTeamType) {
+        PlayerDataProvider.getInstance().storeInventory(player);
+        setDefaultPlayerData(player);
+
+        MWPlayer mwPlayer = addPlayer(player);
+        
+        // Teleport to Lobby and change the gamemode
+        if (game.getState() == GameState.LOBBY) {
+            game.teleportToLobbySpawn(player);
+            player.setGameMode(GameMode.ADVENTURE);
+        }
+        
+        Team team;
+        
+        // Default behavior for new players of this game session:
+        if (targetTeamType == TeamType.SPECTATOR) {
+            team = teamManager.getTeamSpec();
+        } else {
+            team = teamManager.getNextPlayerTeam();
+        }
+        
+        // Was this player already in this game before he left it?
+        if ((game.getState() == GameState.INGAME) || (game.getState() == GameState.END)) {
+            boolean isKnownPlayer = game.getGameLeaveManager().isKnownPlayer(player.getUniqueId());
+            Team lastTeam = game.getGameLeaveManager().getLastTeamOfKnownPlayer(player.getUniqueId());
+            
+            if (isKnownPlayer) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_REJOINED)
+                        .replace("%last-team%", lastTeam.getFullname()));
+                
+                if (lastTeam.getTeamType() == targetTeamType) team = lastTeam;
+            }
+        }
+        
+        team.addMember(mwPlayer);
+        
+        sendJoinBroadcastMsg(mwPlayer);
+        sendJoinPrivateMsg(mwPlayer, false);
+        player.setScoreboard(game.getScoreboardManager().getBoard());
+        
+        if (game.getState() == GameState.LOBBY) {
+            getGameJoinMenu(mwPlayer);
+            
+        } else if (game.getState() == GameState.INGAME) {
+            if (team.getTeamType() == TeamType.PLAYER) startForPlayerAfterCountdown(player, true);
+            if (team.getTeamType() == TeamType.SPECTATOR) startForPlayer(player, true);
+
+        } else {
+            if (team.getTeamType() == TeamType.PLAYER) Logger.ERROR.log("The game-join in the END-phase should not be as player. (Player: " 
+                    + player.getName() + ")");
+            if (team.getTeamType() == TeamType.SPECTATOR) startForPlayer(player, true);
+            
+        }
+    }
+    
+    public void runPlayerTeamSwitch(MWPlayer mwPlayer, Team targetTeam) {
+        Player player = mwPlayer.getPlayer();
+        
+        setDefaultPlayerData(player);
+        
+        // Remove the player from the old team and add him to the new team
+        game.getGameLeaveManager().playerLeaveFromTeam(mwPlayer);
+        targetTeam.addMember(mwPlayer);
+        
+        sendJoinBroadcastMsg(mwPlayer);
+        sendJoinPrivateMsg(mwPlayer, true);
+        // Manual update of the scoreboard because the event listener was not addressed.
+        game.getScoreboardManager().updateScoreboard();
+        
+        if (game.getState() == GameState.LOBBY) {
+            getGameJoinMenu(mwPlayer);
+            
+        } else if (game.getState() == GameState.INGAME) {
+            if (targetTeam.getTeamType() == TeamType.PLAYER) startForPlayerAfterCountdown(player, false);
+            if (targetTeam.getTeamType() == TeamType.SPECTATOR) startForPlayer(player, false);
+
+        } else {
+            if (targetTeam.getTeamType() == TeamType.PLAYER) Logger.ERROR.log("The game-join in the END-phase should not be as player. (Player: " 
+                    + player.getName() + ")");
+            if (targetTeam.getTeamType() == TeamType.SPECTATOR) startForPlayer(player, false);
+            
+        }
+    }
+    
+    private void setDefaultPlayerData(Player player) {
+        player.getInventory().clear();
+        player.setFoodLevel(20);
+        player.setHealth(player.getMaxHealth());
+    }
+    
+    public void startForPlayerAfterCountdown(Player player, boolean isNewPlayer) {
+        MWPlayer mwPlayer = game.getPlayer(player);
+        if (mwPlayer == null) {
+            Logger.ERROR.log("Error starting game at player " + player.getName());
+            return;
+        }
+        
+        player.teleport(mwPlayer.getTeam().getSpawn());
+        player.setGameMode(GameMode.SPECTATOR);
+        
+        runCountdownIntervall(player, "§e5");
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> runCountdownIntervall(player, "§e4"), 20);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> runCountdownIntervall(player, "§e3"), 40);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> runCountdownIntervall(player, "§a2"), 60);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> runCountdownIntervall(player, "§21"), 80);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> startForPlayer(player, isNewPlayer), 100);
+    }
+    
+    private void runCountdownIntervall(Player player, String titel) {
+        player.sendTitle(titel, "", 10, 20, 10);
+        player.playSound(player.getLocation(), Sound.BLOCK_NOTE_BLOCK_PLING, 100, 3);
+    }
+    
+    public void startForPlayer(Player player, boolean isNewPlayer) {
+        MWPlayer mwPlayer = game.getPlayer(player);
+        if (mwPlayer == null) {
+            Logger.ERROR.log("Error starting game at player " + player.getName());
+            return;
+        }
+        
+        player.teleport(mwPlayer.getTeam().getSpawn());
+        
+        if (mwPlayer.getTeam().getTeamType() == TeamType.PLAYER) {
+            // normal team-player join:
+            game.setPlayerAttributes(player);
+            game.getEquipmentManager().sendGameItems(player, false);
+            mwPlayer.iniPlayerEquipmentRandomizer();
+            game.getPlayerTasks().put(player.getUniqueId(), Bukkit.getScheduler().runTaskTimer(MissileWars.getInstance(), mwPlayer, 40, 20));
+            
+        } else {
+            // spectator join:
+            player.setGameMode(GameMode.SPECTATOR);
+            
+            if ((isNewPlayer) && (game.getState() == GameState.INGAME)) {
+                Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> {
+                    openTeamSelectionMenu(mwPlayer);
+                }, 20);
+            }
+            
+        }
+
+    }
+
+    private void sendJoinBroadcastMsg(MWPlayer mwPlayer) {
+        Player player = mwPlayer.getPlayer();
+        
+        String broadcastMsg = null;
+        if (game.getState() == GameState.LOBBY) {
+            broadcastMsg = Messages.getMessage(true, Messages.MessageEnum.LOBBY_PLAYER_JOINED);
+        } else if ((game.getState() == GameState.INGAME) || (game.getState() == GameState.END)) {
+            broadcastMsg = Messages.getMessage(true, Messages.MessageEnum.GAME_PLAYER_JOINED);
+        }
+        
+        if (broadcastMsg != null) {
+            game.broadcast(broadcastMsg.replace("%max_players%", Integer.toString(game.getLobby().getMaxPlayers()))
+                    .replace("%players%", Integer.toString(game.getPlayerAmount()))
+                    .replace("%player%", player.getName())
+                    .replace("%team%", (mwPlayer.getTeam() != null) ? mwPlayer.getTeam().getFullname() : "?"));
+        }
+    }
+    
+    public void sendJoinPrivateMsg(MWPlayer mwPlayer, boolean isTeamSwitch) {
+        Player player = mwPlayer.getPlayer();
+        
+        if (mwPlayer.getTeam() == teamManager.getTeamSpec()) {
+
+            if (isTeamSwitch) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_TEAM_CHANGED)
+                        .replace("%team%", mwPlayer.getTeam().getFullname()));
+            } else {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_TEAM_ASSIGNED)
+                        .replace("%team%", mwPlayer.getTeam().getFullname()));
+            }
+            
+        } else {
+            if (isTeamSwitch) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_TEAM_CHANGED)
+                        .replace("%team%", mwPlayer.getTeam().getFullname()));
+            } else {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_TEAM_ASSIGNED)
+                        .replace("%team%", mwPlayer.getTeam().getFullname()));
+            }
+        }
+    }
+
+    private void getGameJoinMenu(MWPlayer mwPlayer) {
+        mwPlayer.getGameJoinMenu().getMenu();
+    }
+    
+    private void openTeamSelectionMenu(MWPlayer mwPlayer) {
+        mwPlayer.getTeamSelectionMenu().openMenu();
+    }
+    
+    private MWPlayer addPlayer(Player player) {
+        if (game.getPlayers().containsKey(player.getUniqueId())) return game.getPlayers().get(player.getUniqueId());
+        MWPlayer mwPlayer = new MWPlayer(player, game);
+        game.getPlayers().put(player.getUniqueId(), mwPlayer);
+        return mwPlayer;
+    }
+    
+    /**
+     * This method executes the PlayerTeleportEvent to run the basic game join process
+     * after the game is restarted.
+     *
+     * @param player target player
+     */
+    public void runTeleportEventForPlayer(Player player) {
+        Bukkit.getPluginManager().callEvent(new PlayerTeleportEvent(player,
+                Config.getFallbackSpawn(), game.getLobby().getSpawnPoint()));
+    }
+    
+}

+ 138 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameLeaveManager.java

@@ -0,0 +1,138 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.configuration.Messages;
+import de.butzlabben.missilewars.game.enums.GameResult;
+import de.butzlabben.missilewars.game.enums.GameState;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import de.butzlabben.missilewars.player.MWPlayer;
+import de.butzlabben.missilewars.util.PlayerDataProvider;
+import lombok.RequiredArgsConstructor;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+public class GameLeaveManager {
+    
+    private final Game game;
+    private final TeamManager teamManager;
+    
+    private final HashMap<UUID, Team> leftPlayerCache = new HashMap<>();
+    
+    public GameLeaveManager(Game game) {
+        this.game = game;
+        this.teamManager = game.getTeamManager();
+    }
+    
+    /**
+     * This method handles the removal of the player from the game.
+     *
+     * @param mwPlayer the target MissileWars player
+     */
+    public void playerLeaveFromGame(MWPlayer mwPlayer) {
+        Player player = mwPlayer.getPlayer();
+        Team team = mwPlayer.getTeam();
+        
+        playerLeaveFromTeam(mwPlayer);
+        game.removePlayer(mwPlayer);
+        
+        PlayerDataProvider.getInstance().loadInventory(player);
+
+        String message = null;
+        if (game.getState() == GameState.LOBBY) {
+            message = Messages.getMessage(true, Messages.MessageEnum.LOBBY_PLAYER_LEFT);
+        } else if (game.getState() == GameState.INGAME) {
+            message = Messages.getMessage(true, Messages.MessageEnum.GAME_PLAYER_LEFT);
+        }
+
+        if (message != null) {
+            game.broadcast(message.replace("%max_players%", Integer.toString(game.getLobby().getMaxPlayers()))
+                    .replace("%players%", Integer.toString(game.getPlayerAmount()))
+                    .replace("%player%", player.getName())
+                    .replace("%team%", team.getFullname()));
+        }
+
+        player.setScoreboard(Bukkit.getScoreboardManager().getMainScoreboard());
+        
+        if (game.getState() == GameState.LOBBY) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.LOBBY_LEFT).replace("%lobby_name%", game.getLobby().getDisplayName()));
+        } else if (game.getState() == GameState.INGAME) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_LEFT).replace("%arena_name%", game.getArena().getDisplayName()));
+        }
+
+    }
+    
+    public void playerLeaveFromTeam(MWPlayer mwPlayer) {
+        Team oldTeam = mwPlayer.getTeam();
+
+        leftPlayerCache.put(mwPlayer.getUuid(), oldTeam);
+        
+        if (game.getState() == GameState.INGAME) {
+            BukkitTask task = game.getPlayerTasks().get(mwPlayer.getUuid());
+            if (task != null) task.cancel();
+        }
+        
+        oldTeam.removeMember(mwPlayer);
+        if (game.getState() == GameState.INGAME) checkTeamSize(oldTeam);
+    }
+    
+    private void checkTeamSize(Team team) {
+        if (team.getTeamType() == TeamType.SPECTATOR) return;
+        
+        int teamSize = team.getMembers().size();
+        if (teamSize == 0) {
+            Bukkit.getScheduler().runTask(MissileWars.getInstance(), () -> {
+                team.getEnemyTeam().setGameResult(GameResult.WIN);
+                team.setGameResult(GameResult.LOSE);
+                game.sendGameResult();
+                game.stopGame();
+            });
+            game.broadcast(Messages.getMessage(true, Messages.MessageEnum.TEAM_ALL_TEAMMATES_OFFLINE)
+                    .replace("%team%", team.getFullname()));
+        }
+    }
+    
+    /**
+     * This method checks whether the specified player has already played in this game 
+     * before leaving it based of the player-cache.
+     * 
+     * @param uuid (UUID) the target player UUID
+     * @return 'true' if the target player already played in this game
+     */
+    public boolean isKnownPlayer(UUID uuid) {
+        return leftPlayerCache.containsKey(uuid);
+    }
+    
+    /**
+     * This method returns the last team the player was in before leaving the game.
+     * 
+     * @param uuid (UUID) the target player UUID
+     * @return team (Team) the last team of the player (player team or spectator team)
+     */
+    public Team getLastTeamOfKnownPlayer(UUID uuid) {
+        return leftPlayerCache.get(uuid);
+    }
+    
+}

+ 15 - 1
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/MapVoting.java

@@ -109,7 +109,17 @@ public class MapVoting {
 
         return arena;
     }
-
+    
+    public double getPercentOf(Arena arena) {
+        long votes = arenaVotes.values().stream().filter(a -> a.equals(arena)).count();
+        return ((double) votes / arenaVotes.size()) * 100;
+    }
+    
+    public String getPercentOfMsg(Arena arena) {
+        double result = Math.round(getPercentOf(arena));
+        return Double.toString(result);
+    }
+    
     /**
      * This method unlocks the map voting.
      */
@@ -162,4 +172,8 @@ public class MapVoting {
         game.prepareGame();
     }
     
+    public boolean isVotedMapOfPlayer(Arena arena, MWPlayer mwPlayer) {
+        return (arenaVotes.get(mwPlayer) == arena);
+    }
+    
 }

+ 43 - 30
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Team.java

@@ -19,12 +19,14 @@
 package de.butzlabben.missilewars.game;
 
 import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.configuration.Config;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.game.enums.GameResult;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import de.butzlabben.missilewars.menus.MenuItem;
 import de.butzlabben.missilewars.player.MWPlayer;
 import de.butzlabben.missilewars.util.MoneyUtil;
 import de.butzlabben.missilewars.util.version.ColorConverter;
-import java.util.ArrayList;
 import lombok.Getter;
 import lombok.RequiredArgsConstructor;
 import lombok.Setter;
@@ -34,10 +36,12 @@ import org.bukkit.Location;
 import org.bukkit.Material;
 import org.bukkit.Sound;
 import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemFlag;
 import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
 import org.bukkit.inventory.meta.LeatherArmorMeta;
 
+import java.util.ArrayList;
+
 /**
  * @author Butzlabben
  * @since 01.01.2018
@@ -57,17 +61,19 @@ public class Team {
     @Setter private transient GameResult gameResult = GameResult.DRAW;
     private transient int currentInterval = 0;
     ItemStack[] teamArmor;
-
-    public ArrayList<MWPlayer> getMembers() {
-        return members;
+    ItemStack menuItem;
+    
+    public void initialTeam() {
+        createTeamArmor();
+        createMenuItem();
     }
-
+    
     public Team getEnemyTeam() {
-        if (this == game.getTeam1())
-            return game.getTeam2();
-        return game.getTeam1();
+        if (this == game.getTeamManager().getTeam1()) return game.getTeamManager().getTeam2();
+        if (this == game.getTeamManager().getTeam2()) return game.getTeamManager().getTeam1();
+        return null;
     }
-
+    
     public void removeMember(MWPlayer mwPlayer) {
         if (!isMember(mwPlayer)) return;
 
@@ -82,13 +88,9 @@ public class Team {
     }
 
     public void addMember(MWPlayer mwPlayer) {
-        if (isMember(mwPlayer)) return;
-
-        // Already in a team?
-        if (mwPlayer.getTeam() != null) {
-            mwPlayer.getTeam().removeMember(mwPlayer);
-        }
-
+        // Is the player already in a team?
+        if (mwPlayer.getTeam() != null) return;
+        
         Player player = mwPlayer.getPlayer();
         if (player == null) {
             Logger.WARN.log("Could not add player " + mwPlayer.getUuid().toString() + " to a team because he went offline");
@@ -98,6 +100,7 @@ public class Team {
         members.add(mwPlayer);
         mwPlayer.setTeam(this);
         player.setDisplayName(getColorCode() + player.getName() + "§r");
+        
         player.getInventory().setArmorContents(getTeamArmor());
     }
 
@@ -114,7 +117,11 @@ public class Team {
     /**
      * This method creates the team armor based on the team color.
      */
-    public void createTeamArmor() {
+    private void createTeamArmor() {
+        // no armor for spectator
+        if (teamType == TeamType.SPECTATOR) return;
+        
+        
         Color color = ColorConverter.getColorFromCode(getColorCode());
 
         ItemStack boots = new ItemStack(Material.LEATHER_BOOTS);
@@ -143,9 +150,24 @@ public class Team {
 
         teamArmor = new ItemStack[] {boots, leggings, chestplate, helmet};
     }
+    
+    /**
+     * This method creates the team menu-item based on the team color.
+     */
+    private void createMenuItem() {
+        Color color = ColorConverter.getColorFromCode(getColorCode());
+        
+        menuItem = new ItemStack(Material.LEATHER_HELMET);
+        LeatherArmorMeta helmetMeta = (LeatherArmorMeta) menuItem.getItemMeta();
+        helmetMeta.setColor(color);
+        menuItem.setItemMeta(helmetMeta);
+        MenuItem.hideMetaValues(menuItem);
+        MenuItem.setDisplayName(menuItem, Config.TeamSelectionMenuItems.TEAM_ITEM.getMessage()
+                .replace("{player-team-name}", getFullname()));
+    }
 
-    public ItemStack[] getTeamArmor() {
-        return this.teamArmor;
+    public ItemStack getMenuItem() {
+        return menuItem.clone();
     }
 
     public boolean isMember(MWPlayer mwPlayer) {
@@ -237,14 +259,5 @@ public class Team {
             getGame().broadcast(Messages.getMessage(true, Messages.MessageEnum.TEAM_TEAM_NERVED).replace("%team%", getFullname()));
         }
     }
-
-    public ItemStack getGlassPlane() {
-        ItemStack is = new ItemStack(ColorConverter.getGlassPaneFromColorCode(getColorCode()));
-
-        ItemMeta im = is.getItemMeta();
-        im.setDisplayName(getFullname());
-        is.setItemMeta(im);
-        return is;
-    }
-
+    
 }

+ 179 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/TeamManager.java

@@ -0,0 +1,179 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.configuration.lobby.Lobby;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import lombok.Getter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.UUID;
+
+public class TeamManager {
+    
+    private final Game game;
+    private final Lobby lobby;
+    
+    @Getter private Team team1;
+    @Getter private Team team2;
+    @Getter private Team teamSpec;
+    
+    private final Map<UUID, Team> offlinePlayerTeam = new HashMap<>();
+    
+    public TeamManager(Game game) {
+        this.game = game;
+        this.lobby = game.getLobby();
+        
+        team1 = new Team(lobby.getTeam1Config().getName(), lobby.getTeam1Config().getColor(), game, TeamType.PLAYER);
+        team2 = new Team(lobby.getTeam2Config().getName(), lobby.getTeam2Config().getColor(), game, TeamType.PLAYER);
+        teamSpec = new Team(lobby.getTeamConfigSpec().getName(), lobby.getTeamConfigSpec().getColor(), game, TeamType.SPECTATOR);
+        
+        team1.initialTeam();
+        team2.initialTeam();
+        teamSpec.initialTeam();
+    }
+    
+    /**
+     * This method provides the next best team that a new player should be 
+     * added into, so that the game remains/becomes balanced in terms of team 
+     * sizes. Depending on the team size ratio, either the smaller or a 
+     * random team is returned.
+     * 
+     * @return (Team) the next team for balanced game sizes
+     */
+    public Team getNextPlayerTeam() {
+        if (isSamePlayerTeamSize()) {
+            Random randomizer = new Random();
+            
+            if (randomizer.nextBoolean()) {
+                return team1;
+            } else {
+                return team2;
+            }
+        }
+        
+        return getSmallerPlayerTeam();
+    }
+
+    /**
+     * This method returns the smaller player-team of Team1 and Team2.
+     *
+     * @return (Team) the smaller team
+     */
+    public Team getSmallerPlayerTeam() {
+        if (team1.getMembers().size() > team2.getMembers().size()) {
+            return team2;
+        } else if (team1.getMembers().size() < team2.getMembers().size()) {
+            return team1;
+        }
+        
+        return null;
+    }
+    
+    /**
+     * This method returns the larger player-team of Team1 and Team2.
+     *
+     * @return (Team) the larger team
+     */
+    public Team getLargerPlayerTeam() {
+        if (team1.getMembers().size() < team2.getMembers().size()) {
+            return team2;
+        } else if (team1.getMembers().size() > team2.getMembers().size()) {
+            return team1;
+        }
+        
+        return null;
+    }
+    
+    private boolean isSamePlayerTeamSize() {
+        return (team1.getMembers().size() == team2.getMembers().size());
+    }
+    
+    public boolean hasEmptyPlayerTeam() {
+        return ((team1.getMembers().isEmpty()) || (team2.getMembers().isEmpty()));
+    }
+    
+    /**
+     * This method checks whether a team switch would be fair based on 
+     * the new team size. If no empty team results or if the team size 
+     * difference does not exceed a certain value, the switch is 
+     * considered acceptable.
+     *
+     * @param currentTeam the current team of the player
+     * @param targetTeam the desired team
+     * @return (boolean) 'true' if it's a fair team switch
+     */
+    public boolean isValidFairSwitch(Team currentTeam, Team targetTeam) {
+        if (targetTeam.getTeamType() == TeamType.SPECTATOR) return true;
+        
+        // Prevention of an empty team in some cases.
+        // This should only be relevant if the method is also queried in the lobby.
+        if (currentTeam.getTeamType() == TeamType.PLAYER) {
+            if ((currentTeam.getMembers().size() == 1) && (!targetTeam.getMembers().isEmpty())) {
+                Logger.DEBUG.log("Prevent team switch! Current player-team size: " + currentTeam.getMembers().size() 
+                        + "; target player-team size: " + targetTeam.getMembers().size());
+                return false;
+            }
+        } else {
+            if ((!targetTeam.getMembers().isEmpty()) && (targetTeam.getEnemyTeam().getMembers().isEmpty())) {
+                Logger.DEBUG.log("Prevent team switch! Target player-team size: " + targetTeam.getMembers().size() 
+                        + "; enemy player-team size: " + targetTeam.getEnemyTeam().getMembers().size());
+                return false;
+            }
+        }
+        
+        if (targetTeam == getSmallerPlayerTeam()) return true;
+        
+        // The person change is "pre-simulated" here (thus working with the new team sizes) 
+        // to take into account the negative exponential influence of one player in relation 
+        // to the team size.
+        
+        // prospective team sizes:
+        int newTargetTeamSize = targetTeam.getMembers().size() + 1;
+        int newEnemyTeamSize = currentTeam.getMembers().size();
+        if (currentTeam.getTeamType() == TeamType.PLAYER) newEnemyTeamSize--;
+        
+        int diff = Math.abs(newTargetTeamSize - newEnemyTeamSize);
+        
+        // max team difference: XX% of target team size, minimal value = 1
+        double maxDiff = Math.max(Math.max(newTargetTeamSize, newEnemyTeamSize) * 0.45, 1);
+        
+        if (diff <= maxDiff) return true;
+        
+        Logger.DEBUG.log("Prevent team switch! Max team difference: " + maxDiff + "; current difference: " + diff);
+        return false;
+    }
+    
+    public boolean hasBalancedTeamSizes() {
+        if (hasEmptyPlayerTeam()) return false;
+        
+        int diff = Math.abs(team1.getMembers().size() - team2.getMembers().size());
+        
+        // max team difference: XX% of target team size, minimal value = 1
+        double maxDiff = Math.max(Math.max(team1.getMembers().size(), team2.getMembers().size()) * 0.35, 1);
+        
+        if (diff <= maxDiff) return true;
+        
+        Logger.DEBUG.log("Prevent game start! Max team difference: " + maxDiff + "; current difference: " + diff);
+        return false;
+    }
+}

+ 30 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/JoinIngameBehavior.java

@@ -0,0 +1,30 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.enums;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+public enum JoinIngameBehavior {
+
+    FORBIDDEN,
+    SPECTATOR,
+    PLAYER
+}

+ 31 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/RejoinIngameBehavior.java

@@ -0,0 +1,31 @@
+/*
+ * This file is part of MissileWars (https://github.com/Butzlabben/missilewars).
+ * Copyright (c) 2018-2021 Daniel Nägele.
+ *
+ * MissileWars 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.
+ *
+ * MissileWars 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 MissileWars.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package de.butzlabben.missilewars.game.enums;
+
+/**
+ * @author Butzlabben
+ * @since 01.01.2018
+ */
+public enum RejoinIngameBehavior {
+
+    FORBIDDEN,
+    SPECTATOR,
+    PLAYER,
+    LAST_TEAM
+}

+ 5 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MissileWarsPlaceholder.java

@@ -104,6 +104,11 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
             if (params.equalsIgnoreCase("lobby_gamestate_" + lobby.getName())) {
                 return GameManager.getInstance().getGameStateMessage(game);
             }
+            
+            // %missilewars_lobby_mapvote_state_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_mapvote_state_" + lobby.getName())) {
+                return game.getMapVoting().getState().toString();
+            }
 
             // %missilewars_lobby_displayname_<lobby name or 'this'>%
             if (params.equalsIgnoreCase("lobby_displayname_" + lobby.getName())) {

+ 3 - 5
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MotdManager.java

@@ -51,12 +51,10 @@ public class MotdManager {
                     newMotd = Config.motdGame();
                     break;
             }
-
-            int players = game.getPlayers().values().size();
-            int maxPlayers = game.getLobby().getMaxSize();
+            
             motd = ChatColor.translateAlternateColorCodes('&', newMotd)
-                    .replace("%max_players%", Integer.toString(maxPlayers))
-                    .replace("%players%", Integer.toString(players))
+                    .replace("%max_players%", Integer.toString(game.getLobby().getMaxPlayers()))
+                    .replace("%players%", Integer.toString(game.getPlayerAmount()))
                     .replace("%prefix%", Messages.getPrefix());
         }
     }

+ 4 - 21
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/ScoreboardManager.java

@@ -48,9 +48,7 @@ public class ScoreboardManager {
 
     @Setter
     private String arenaDisplayName;
-    @Setter
-    private String arenaGameDuration;
-
+    
     // get config options
     private static final String SCOREBOARD_TITLE = Config.getScoreboardTitle();
     private static final String MEMBER_LIST_STYLE = Config.getScoreboardMembersStyle();
@@ -70,16 +68,14 @@ public class ScoreboardManager {
      */
     private void createScoreboard() {
 
-        team1 = game.getTeam1();
-        team2 = game.getTeam2();
+        team1 = game.getTeamManager().getTeam1();
+        team2 = game.getTeamManager().getTeam2();
 
         if (game.getArena() == null) {
             // using of placeholders until the arena is not set
             setArenaDisplayName("?");
-            setArenaGameDuration("0");
         } else {
             setArenaDisplayName(game.getArena().getDisplayName());
-            setArenaGameDuration(Integer.toString(game.getArena().getGameDuration()));
         }
 
         // register Scoreboard
@@ -243,19 +239,6 @@ public class ScoreboardManager {
      * @return the replaced text as String
      */
     private String replaceScoreboardPlaceholders(String text) {
-
-        String time = "";
-        if (game.getState() == GameState.LOBBY) {
-            // Show the planned duration of the next game:
-            time = arenaGameDuration;
-        } else if (game.getState() == GameState.INGAME) {
-            // Show the remaining duration of the running game:
-            time = Integer.toString(game.getTaskManager().getTimer().getSeconds() / 60);
-        } else if (game.getState() == GameState.END) {
-            // Show the remaining duration of the last game:
-            time = Integer.toString(game.getRemainingGameDuration() / 60);
-        }
-        
         
         text = text.replace("%team1%", team1.getFullname());
         text = text.replace("%team2%", team2.getFullname());
@@ -269,7 +252,7 @@ public class ScoreboardManager {
         text = text.replace("%lobby_name%", game.getLobby().getDisplayName());
         text = text.replace("%arena_name%", arenaDisplayName);
 
-        text = text.replace("%time%", time);
+        text = text.replace("%time%", Integer.toString(game.getGameDuration()));
 
         return text;
     }

+ 2 - 2
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/signs/MWSign.java

@@ -101,8 +101,8 @@ public class MWSign {
             }
         }
 
-        int maxPlayers = (game == null ? 0 : game.getLobby().getMaxSize());
-        int players = (game == null ? 0 : game.getPlayers().size());
+        int maxPlayers = (game == null ? 0 : game.getLobby().getMaxPlayers());
+        int players = (game == null ? 0 : game.getPlayerAmount());
 
         return line.replace("%state%", state)
                 .replace("%arena%", name)

+ 3 - 3
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/stats/FightStats.java

@@ -54,9 +54,9 @@ public class FightStats {
      */
     private int getGameResultCode() {
 
-        if (game.getTeam1().getGameResult() == GameResult.WIN) {
+        if (game.getTeamManager().getTeam1().getGameResult() == GameResult.WIN) {
             return 1;
-        } else if (game.getTeam2().getGameResult() == GameResult.WIN) {
+        } else if (game.getTeamManager().getTeam2().getGameResult() == GameResult.WIN) {
             return 2;
         }
 
@@ -109,7 +109,7 @@ public class FightStats {
                     statement.setInt(1, fightID);
                     statement.setString(2, mwPlayer.getUuid().toString());
 
-                    if (mwPlayer.getTeam() == game.getTeam1())
+                    if (mwPlayer.getTeam() == game.getTeamManager().getTeam1())
                         statement.setInt(3, 1);
                     else
                         statement.setInt(3, 2);

+ 3 - 9
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java

@@ -49,7 +49,7 @@ public class LobbyTimer extends Timer implements Runnable {
             mwPlayer.getPlayer().setLevel(seconds);
         }
         
-        if (hasEmptyPlayerTeam()) {
+        if (getGame().getTeamManager().hasEmptyPlayerTeam()) {
             seconds = startTime;
             return;
         }
@@ -83,7 +83,7 @@ public class LobbyTimer extends Timer implements Runnable {
                 playPling();
                 break;
             case 0:
-                if (hasEmptyPlayerTeam()) {
+                if (!getGame().getTeamManager().hasBalancedTeamSizes()) {
                     broadcast(Messages.getMessage(true, Messages.MessageEnum.LOBBY_TEAMS_UNEQUAL));
                     seconds = startTime;
                     return;
@@ -108,15 +108,9 @@ public class LobbyTimer extends Timer implements Runnable {
      * are informed about the start.
      */
     public void executeGameStart() {
-        broadcast(Messages.getMessage(true, Messages.MessageEnum.LOBBY_GAME_STARTS));
+        broadcast(Messages.getMessage(true, Messages.MessageEnum.GAME_GAME_STARTS));
         playPling();
         getGame().startGame();
     }
     
-    private boolean hasEmptyPlayerTeam() {
-        int size1 = getGame().getTeam1().getMembers().size();
-        int size2 = getGame().getTeam2().getMembers().size();
-        
-        return ((size1 == 0) || (size2 == 0));
-    }
 }

+ 2 - 2
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/PlayerListener.java

@@ -197,8 +197,8 @@ public class PlayerListener implements Listener {
         Logger.DEBUG.log("Location: " + player.getLocation());
         Logger.DEBUG.log("Current game amount: " + GameManager.getInstance().getGameAmount());
         Logger.DEBUG.log("Lobby: " + game.getLobby().getDisplayName());
-        Logger.DEBUG.log("Team 1: " + game.getTeam1());
-        Logger.DEBUG.log("Team 2: " + game.getTeam2());
+        Logger.DEBUG.log("Team 1: " + game.getTeamManager().getTeam1());
+        Logger.DEBUG.log("Team 2: " + game.getTeamManager().getTeam2());
 
         if (game.getArena() != null) {
             Logger.DEBUG.log("Arena: " + game.getArena().getDisplayName());

+ 57 - 6
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/EndListener.java

@@ -18,10 +18,17 @@
 
 package de.butzlabben.missilewars.listener.game;
 
+import de.butzlabben.missilewars.Logger;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.event.PlayerArenaJoinEvent;
 import de.butzlabben.missilewars.event.PlayerArenaLeaveEvent;
 import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.GameLeaveManager;
+import de.butzlabben.missilewars.game.Team;
+import de.butzlabben.missilewars.game.enums.JoinIngameBehavior;
+import de.butzlabben.missilewars.game.enums.RejoinIngameBehavior;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import de.butzlabben.missilewars.menus.inventory.TeamSelectionMenu;
 import de.butzlabben.missilewars.player.MWPlayer;
 import org.bukkit.GameMode;
 import org.bukkit.entity.Player;
@@ -30,6 +37,7 @@ import org.bukkit.event.EventPriority;
 import org.bukkit.event.entity.PlayerDeathEvent;
 import org.bukkit.event.inventory.InventoryClickEvent;
 import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.event.inventory.InventoryType;
 import org.bukkit.event.player.PlayerRespawnEvent;
 
 /**
@@ -64,6 +72,9 @@ public class EndListener extends GameBoundListener {
         Player player = (Player) event.getPlayer();
         if (!isInGameWorld(player.getLocation())) return;
 
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle())) return;
+        
         if (player.getGameMode() != GameMode.CREATIVE) event.setCancelled(true);
     }
 
@@ -74,21 +85,61 @@ public class EndListener extends GameBoundListener {
         Player player = (Player) event.getWhoClicked();
         if (!isInGameWorld(player.getLocation())) return;
 
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle())) {
+            if (event.getSlotType() == InventoryType.SlotType.CONTAINER) return;
+        }
+        
         if (player.getGameMode() != GameMode.CREATIVE) event.setCancelled(true);
+        Logger.DEBUG.log("Cancelled 'InventoryClickEvent' event of " + player.getName());
     }
 
     @EventHandler
     public void onPlayerArenaJoin(PlayerArenaJoinEvent event) {
         if (!getGame().isIn(event.getPlayer().getLocation())) return;
+        
+        Player player = event.getPlayer();
 
-        if (getGame().isSpectatorsMax()) {
-            event.setCancelled(true);
+        JoinIngameBehavior joinBehavior = getGame().getLobby().getJoinIngameBehavior();
+        RejoinIngameBehavior rejoinBehavior = getGame().getLobby().getRejoinIngameBehavior();
+        boolean isKnownPlayer = getGame().getGameLeaveManager().isKnownPlayer(player.getUniqueId());
+        Team lastTeam = getGame().getGameLeaveManager().getLastTeamOfKnownPlayer(player.getUniqueId());
+        
+        // A: Forbidden the game join:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.FORBIDDEN) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.FORBIDDEN)) {
             event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_ENTER_ARENA));
+            event.setCancelled(true);
             return;
         }
-
-        Player player = event.getPlayer();
-        getGame().playerJoinInGame(player, true);
+        
+        // B: game join in a player-team --> Forcing the join as a spectator because of the ENDGAME phase:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.PLAYER) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.PLAYER) 
+                || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.LAST_TEAM && lastTeam.getTeamType() == TeamType.PLAYER)) {
+            
+            if (!getGame().areTooManySpectators()) {
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.SPECTATOR);
+                
+            } else {
+                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_MAX_REACHED));
+                event.setCancelled(true);
+                
+            }
+            return;
+        }
+        
+        // C: game join in a spectator-team:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.SPECTATOR) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.SPECTATOR) 
+                || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.LAST_TEAM && lastTeam.getTeamType() == TeamType.SPECTATOR)) {
+            
+            if (!getGame().areTooManySpectators()) {
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.SPECTATOR);
+                
+            } else {
+                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_MAX_REACHED));
+                event.setCancelled(true);
+                
+            }
+        }
     }
 
     @EventHandler
@@ -98,6 +149,6 @@ public class EndListener extends GameBoundListener {
         Player player = event.getPlayer();
         MWPlayer mwPlayer = event.getGame().getPlayer(player);
 
-        if (mwPlayer != null) getGame().playerLeaveFromGame(mwPlayer);
+        if (mwPlayer != null) getGame().getGameLeaveManager().playerLeaveFromGame(mwPlayer);
     }
 }

+ 72 - 13
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/GameListener.java

@@ -18,6 +18,7 @@
 
 package de.butzlabben.missilewars.listener.game;
 
+import de.butzlabben.missilewars.Logger;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.configuration.arena.FallProtectionConfiguration;
 import de.butzlabben.missilewars.event.PlayerArenaJoinEvent;
@@ -25,9 +26,13 @@ import de.butzlabben.missilewars.event.PlayerArenaLeaveEvent;
 import de.butzlabben.missilewars.game.Game;
 import de.butzlabben.missilewars.game.Team;
 import de.butzlabben.missilewars.game.enums.GameResult;
+import de.butzlabben.missilewars.game.enums.JoinIngameBehavior;
+import de.butzlabben.missilewars.game.enums.RejoinIngameBehavior;
+import de.butzlabben.missilewars.game.enums.TeamType;
 import de.butzlabben.missilewars.game.misc.RespawnGoldBlock;
 import de.butzlabben.missilewars.game.schematics.objects.Missile;
 import de.butzlabben.missilewars.listener.ShieldListener;
+import de.butzlabben.missilewars.menus.inventory.TeamSelectionMenu;
 import de.butzlabben.missilewars.player.MWPlayer;
 import de.butzlabben.missilewars.util.geometry.Geometry;
 import org.bukkit.GameMode;
@@ -79,8 +84,8 @@ public class GameListener extends GameBoundListener {
 
         Location location = event.getBlock().getLocation();
 
-        Team team1 = getGame().getTeam1();
-        Team team2 = getGame().getTeam2();
+        Team team1 = getGame().getTeamManager().getTeam1();
+        Team team2 = getGame().getTeamManager().getTeam2();
 
         if (Geometry.isCloser(location, team1.getSpawn(), team2.getSpawn())) {
             team1.setGameResult(GameResult.LOSE);
@@ -230,6 +235,9 @@ public class GameListener extends GameBoundListener {
         Player player = (Player) event.getPlayer();
         if (!isInGameWorld(player.getLocation())) return;
 
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle())) return;
+        
         if (player.getGameMode() == GameMode.CREATIVE) return;
         if (player.getGameMode() == GameMode.SPECTATOR) event.setCancelled(true);
 
@@ -248,16 +256,30 @@ public class GameListener extends GameBoundListener {
         Player player = (Player) event.getWhoClicked();
         if (!isInGameWorld(player.getLocation())) return;
 
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle())) {
+            if (event.getSlotType() == InventoryType.SlotType.CONTAINER) return;
+        }
+        
         if (player.getGameMode() == GameMode.CREATIVE) return;
-        if (player.getGameMode() == GameMode.SPECTATOR) event.setCancelled(true);
+        if (player.getGameMode() == GameMode.SPECTATOR) {
+            event.setCancelled(true);
+            Logger.DEBUG.log("Cancelled 'InventoryClickEvent' event of " + player.getName());
+        }
 
         Inventory clickedInventory = event.getClickedInventory();
         if (clickedInventory != null) {
-            if (clickedInventory.getType() != InventoryType.PLAYER) event.setCancelled(true);
+            if (clickedInventory.getType() != InventoryType.PLAYER) {
+                event.setCancelled(true);
+                Logger.DEBUG.log("Cancelled 'InventoryClickEvent' event of " + player.getName());
+            }
         }
 
         if ((event.getSlotType() != InventoryType.SlotType.CONTAINER) &&
-                (event.getSlotType() != InventoryType.SlotType.QUICKBAR)) event.setCancelled(true);
+                (event.getSlotType() != InventoryType.SlotType.QUICKBAR)) {
+            event.setCancelled(true);
+            Logger.DEBUG.log("Cancelled 'InventoryClickEvent' event of " + player.getName());
+        }
     }
 
     @EventHandler
@@ -291,17 +313,54 @@ public class GameListener extends GameBoundListener {
 
         Player player = event.getPlayer();
 
-        if ((!getGame().getLobby().isJoinOngoingGame()) || (getGame().isPlayersMax())) {
-            if (getGame().isSpectatorsMax()) {
+        JoinIngameBehavior joinBehavior = getGame().getLobby().getJoinIngameBehavior();
+        RejoinIngameBehavior rejoinBehavior = getGame().getLobby().getRejoinIngameBehavior();
+        boolean isKnownPlayer = getGame().getGameLeaveManager().isKnownPlayer(player.getUniqueId());
+        Team lastTeam = getGame().getGameLeaveManager().getLastTeamOfKnownPlayer(player.getUniqueId());
+        
+        // A: Forbidden the game join:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.FORBIDDEN) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.FORBIDDEN)) {
+            event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_ENTER_ARENA));
+            event.setCancelled(true);
+            return;
+        }
+        
+        // B: game join in a player-team:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.PLAYER) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.PLAYER) 
+                || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.LAST_TEAM && lastTeam.getTeamType() == TeamType.PLAYER)) {
+            
+            if (!getGame().areTooManyPlayers()) {
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.PLAYER);
+                
+            } else if (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.LAST_TEAM && lastTeam.getTeamType() == TeamType.PLAYER 
+                    && !getGame().areTooManySpectators()) {
+                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_MAX_REACHED));
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.SPECTATOR);
+                
+            } else {
+                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_MAX_REACHED));
                 event.setCancelled(true);
-                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_ENTER_ARENA));
-                return;
+                
             }
-            getGame().playerJoinInGame(player, true);
             return;
         }
-
-        getGame().playerJoinInGame(player, false);
+        
+        // C: game join in a spectator-team:
+        if ((!isKnownPlayer && joinBehavior == JoinIngameBehavior.SPECTATOR) || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.SPECTATOR) 
+                || (isKnownPlayer && rejoinBehavior == RejoinIngameBehavior.LAST_TEAM && lastTeam.getTeamType() == TeamType.SPECTATOR)) {
+            
+            if (!getGame().areTooManySpectators()) {
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.SPECTATOR);
+
+            } else if (!getGame().areTooManyPlayers()) {
+                getGame().getGameJoinManager().runPlayerJoin(player, TeamType.PLAYER);
+                
+            } else {
+                event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_SPECTATOR_MAX_REACHED));
+                event.setCancelled(true);
+                
+            }
+        }
     }
 
     @EventHandler
@@ -311,6 +370,6 @@ public class GameListener extends GameBoundListener {
         Player player = event.getPlayer();
         MWPlayer mwPlayer = event.getGame().getPlayer(player);
 
-        if (mwPlayer != null) getGame().playerLeaveFromGame(mwPlayer);
+        if (mwPlayer != null) getGame().getGameLeaveManager().playerLeaveFromGame(mwPlayer);
     }
 }

+ 68 - 36
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/LobbyListener.java

@@ -18,22 +18,27 @@
 
 package de.butzlabben.missilewars.listener.game;
 
+import de.butzlabben.missilewars.Logger;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.event.PlayerArenaJoinEvent;
 import de.butzlabben.missilewars.event.PlayerArenaLeaveEvent;
 import de.butzlabben.missilewars.game.Game;
-import de.butzlabben.missilewars.inventory.VoteInventory;
+import de.butzlabben.missilewars.game.enums.TeamType;
+import de.butzlabben.missilewars.menus.MenuItem;
+import de.butzlabben.missilewars.menus.hotbar.GameJoinMenu;
+import de.butzlabben.missilewars.menus.inventory.MapVoteMenu;
+import de.butzlabben.missilewars.menus.inventory.TeamSelectionMenu;
 import de.butzlabben.missilewars.player.MWPlayer;
 import org.bukkit.GameMode;
-import org.bukkit.Material;
 import org.bukkit.entity.Player;
 import org.bukkit.event.EventHandler;
 import org.bukkit.event.EventPriority;
+import org.bukkit.event.block.Action;
 import org.bukkit.event.entity.EntityDamageEvent;
-import org.bukkit.event.inventory.InventoryClickEvent;
-import org.bukkit.event.inventory.InventoryOpenEvent;
+import org.bukkit.event.inventory.*;
 import org.bukkit.event.player.PlayerInteractEvent;
 import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.event.player.PlayerSwapHandItemsEvent;
 
 /**
  * @author Butzlabben
@@ -59,24 +64,27 @@ public class LobbyListener extends GameBoundListener {
         setInteractDelay(player);
 
         if (event.getItem() == null) return;
-
-        if (event.getItem().getType().name().contains("STAINED_GLASS_PANE")) {
-            // team change:
-            if (!player.hasPermission("mw.change")) return;
-
-            String displayName = event.getItem().getItemMeta().getDisplayName();
-            if (displayName.equals(getGame().getTeam1().getFullname())) {
-                player.performCommand("mw change 1");
-            } else {
-                player.performCommand("mw change 2");
-            }
-
-        } else if (event.getItem().getType() == Material.NETHER_STAR) {
-            // vote inventory:
-            if (player.hasPermission("mw.vote")) {
-                VoteInventory inventory = new VoteInventory(getGame().getLobby().getArenas());
-                player.openInventory(inventory.getInventory(player));
-            }
+        
+        // execution commands from the hotbar menu items
+        int slotId = player.getInventory().getHeldItemSlot();
+        if (!GameJoinMenu.menuItems.containsKey(slotId)) return;
+        
+        if ((event.getAction().equals(Action.LEFT_CLICK_AIR)) || (event.getAction().equals(Action.LEFT_CLICK_BLOCK))) {
+            
+            MWPlayer mwPlayer = getGame().getPlayer(player);
+            if (!mwPlayer.getGameJoinMenu().finalMenuItems.containsKey(slotId)) return;
+            
+            MenuItem menuItem = mwPlayer.getGameJoinMenu().finalMenuItems.get(slotId);
+            menuItem.getLeftClickActions().runActions(player, getGame());
+        }
+        
+        if ((event.getAction().equals(Action.RIGHT_CLICK_AIR)) || (event.getAction().equals(Action.RIGHT_CLICK_BLOCK))) {
+            
+            MWPlayer mwPlayer = getGame().getPlayer(player);
+            if (!mwPlayer.getGameJoinMenu().finalMenuItems.containsKey(slotId)) return;
+            
+            MenuItem menuItem = mwPlayer.getGameJoinMenu().finalMenuItems.get(slotId);
+            menuItem.getRightClickActions().runActions(player, getGame());
         }
     }
 
@@ -102,9 +110,10 @@ public class LobbyListener extends GameBoundListener {
         Player player = (Player) event.getPlayer();
         if (!isInLobbyArea(player.getLocation())) return;
         
-        // handling of vote inventory:
-        if (event.getView().getTitle().equals(Messages.getMessage(false, Messages.MessageEnum.VOTE_GUI))) return;
-
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle()) || 
+                event.getView().getTitle().equals(MapVoteMenu.getTitle())) return;
+        
         if (player.getGameMode() != GameMode.CREATIVE) event.setCancelled(true);
     }
 
@@ -114,24 +123,47 @@ public class LobbyListener extends GameBoundListener {
 
         Player player = (Player) event.getWhoClicked();
         if (!isInLobbyArea(player.getLocation())) return;
-
-        // handling of vote inventory: see 'VoteInventory.class'
+        
+        // handling of MW inventories:
+        if (event.getView().getTitle().equals(TeamSelectionMenu.getTitle()) || 
+                event.getView().getTitle().equals(MapVoteMenu.getTitle())) {
+            if (event.getSlotType() == InventoryType.SlotType.CONTAINER) return;
+        }
         
         if (player.getGameMode() != GameMode.CREATIVE) event.setCancelled(true);
+        Logger.DEBUG.log("Cancelled 'InventoryClickEvent' event of " + player.getName());
     }
-
+    
+    @EventHandler
+    public void onPlayerSwapHandItems(PlayerSwapHandItemsEvent event) {
+        if (!isInLobbyArea(event.getPlayer().getLocation())) return;
+        
+        Player player = event.getPlayer();
+        
+        if (player.getGameMode() != GameMode.CREATIVE) event.setCancelled(true);
+        Logger.DEBUG.log("Cancelled 'PlayerSwapHandItemsEvent' event of " + player.getName());
+    }
+    
     @EventHandler
     public void onPlayerArenaJoin(PlayerArenaJoinEvent event) {
         if (!isInLobbyArea(event.getPlayer().getLocation())) return;
-
-        if (getGame().isPlayersMax()) {
+        
+        Player player = event.getPlayer();
+        
+        // A: game join in a player-team:
+        if (!getGame().areTooManyPlayers()) {
+            getGame().getGameJoinManager().runPlayerJoin(player, TeamType.PLAYER);
+            
+        } else if (!getGame().areTooManySpectators()) {
+            event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_MAX_REACHED));
+            getGame().getGameJoinManager().runPlayerJoin(player, TeamType.SPECTATOR);
+            
+        } else {
+            event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_MAX_REACHED));
             event.setCancelled(true);
-            event.getPlayer().sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_ENTER_ARENA));
-            return;
+            
         }
-
-        Player player = event.getPlayer();
-        getGame().playerJoinInGame(player, false);
+        
     }
 
     @EventHandler
@@ -141,6 +173,6 @@ public class LobbyListener extends GameBoundListener {
         Player player = event.getPlayer();
         MWPlayer mwPlayer = event.getGame().getPlayer(player);
 
-        if (mwPlayer != null) getGame().playerLeaveFromGame(mwPlayer);
+        if (mwPlayer != null) getGame().getGameLeaveManager().playerLeaveFromGame(mwPlayer);
     }
 }

+ 111 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/ItemRequirement.java

@@ -0,0 +1,111 @@
+package de.butzlabben.missilewars.menus;
+
+import de.butzlabben.missilewars.configuration.Messages;
+import de.butzlabben.missilewars.player.MWPlayer;
+import org.bukkit.configuration.ConfigurationSection;
+
+public class ItemRequirement {
+    
+    private final Type type;
+    
+    private String permission = "";
+    private String input = "";
+    private String output = "";
+    private boolean negateRequirement;
+    
+    public ItemRequirement(ConfigurationSection cfg) {
+        String type = cfg.getString("type");
+        this.type = getType(type);
+        
+        getCfgValues(cfg);
+    }
+    
+    public ItemRequirement() {
+        this.type = Type.NULL;
+    }
+    
+    public boolean isRequirementsGiven(MWPlayer mwPlayer) {
+        if (type == Type.NULL) return true;
+
+        boolean result = false;
+        String finalPermission = Messages.getPapiMessage(permission, mwPlayer.getPlayer());
+        String finalInput = Messages.getPapiMessage(input, mwPlayer.getPlayer());
+        String finalOutput = Messages.getPapiMessage(output, mwPlayer.getPlayer());
+        
+        if (type == Type.HAS_PERMISSION) {
+            if ((finalPermission.isEmpty()) || (finalPermission.isBlank())) return false;
+            
+            result = mwPlayer.getPlayer().hasPermission(finalPermission);
+            
+        } else if (type == Type.STRING_EQUALS) {
+            if ((finalInput.isEmpty()) || (finalInput.isBlank())) return false;
+            if (finalOutput.isEmpty()) return false;
+            
+            result = finalInput.equals(finalOutput);
+            
+        } else if (type == Type.STRING_EQUALS_IGNORE_CASE) {
+            if ((finalInput.isEmpty()) || (finalInput.isBlank())) return false;
+            if (finalOutput.isEmpty()) return false;
+            
+            result = finalInput.equalsIgnoreCase(finalOutput);
+            
+        } else if (type == Type.STRING_CONTAINS) {
+            if ((finalInput.isEmpty()) || (finalInput.isBlank())) return false;
+            if (finalOutput.isEmpty()) return false;
+            
+            result = finalInput.contains(finalOutput);
+            
+        }
+        
+        if (negateRequirement) return !result;
+        return result;
+    }
+    
+    private Type getType(String input) {
+        switch (input) {
+            case "!has permission":
+                negateRequirement = true;
+                return Type.HAS_PERMISSION;
+            case "has permission":
+                return Type.HAS_PERMISSION;
+            case "!string equals":
+                negateRequirement = true;
+                return Type.STRING_EQUALS;
+            case "string equals":
+                return Type.STRING_EQUALS;
+            case "!string equals ignorecase":
+                negateRequirement = true;
+                return Type.STRING_EQUALS_IGNORE_CASE;
+            case "string equals ignorecase":
+                return Type.STRING_EQUALS_IGNORE_CASE;
+            case "!string contains":
+                negateRequirement = true;
+                return Type.STRING_CONTAINS;
+            case "string contains":
+                return Type.STRING_CONTAINS;
+            default: return Type.NULL;
+        }
+    }
+    
+    private void getCfgValues(ConfigurationSection cfg) {
+        switch (type) {
+            case HAS_PERMISSION:
+                permission = cfg.getString("permission");
+                break;
+            case STRING_EQUALS:
+            case STRING_EQUALS_IGNORE_CASE:
+            case STRING_CONTAINS: 
+                input = cfg.getString("input");
+                output = cfg.getString("output");
+        }
+    }
+    
+    enum Type {
+        NULL,
+        HAS_PERMISSION,
+        STRING_EQUALS,
+        STRING_EQUALS_IGNORE_CASE,
+        STRING_CONTAINS,
+    }
+    
+}

+ 150 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuItem.java

@@ -0,0 +1,150 @@
+package de.butzlabben.missilewars.menus;
+
+import com.mojang.authlib.GameProfile;
+import com.mojang.authlib.properties.Property;
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.configuration.ActionSet;
+import de.butzlabben.missilewars.configuration.Messages;
+import de.butzlabben.missilewars.player.MWPlayer;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import org.bukkit.Material;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.inventory.meta.SkullMeta;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+public class MenuItem {
+    
+    @Getter private final int slot;
+    @Getter private final int priority;
+
+    @Getter private ItemRequirement itemRequirement;
+
+    @Getter @Setter private String materialName;
+    
+    @Setter private String displayName;
+    @Getter private String finalDisplayName;
+    
+    @Setter private List<String> loreList;
+    @Getter private final List<String> finalLoreList = new ArrayList<>();
+    
+    @Getter @Setter private ActionSet leftClickActions;
+    @Getter @Setter private ActionSet rightClickActions;
+    
+    private ItemStack itemStack;
+    
+    
+    public void updateItem(MWPlayer mwPlayer) {
+        
+        ItemStack tempItem;
+        
+        // basehead-<base64 (Value field in the head's give command)>
+        if (materialName.startsWith("basehead-")) {
+            tempItem = getCustomHead(materialName.split("-")[1]);
+            
+        } else if (materialName.equalsIgnoreCase("{player-team-name}")) {
+            tempItem = mwPlayer.getTeam().getMenuItem();
+            
+        } else {
+            tempItem = new ItemStack(Material.valueOf(materialName.toUpperCase()));
+            
+        }
+        
+        updatePapiValues(mwPlayer.getPlayer());
+        
+        // initial ItemMeta values:
+        
+        ItemMeta itemMeta = tempItem.getItemMeta();
+        MenuItem.hideMetaValues(tempItem);
+        MenuItem.setDisplayName(tempItem, finalDisplayName);
+        itemMeta.setLore(finalLoreList);
+        tempItem.setItemMeta(itemMeta);
+        
+        itemStack = tempItem;
+    }
+    
+    public void sendToPlayer(MWPlayer mwPlayer) {
+        updateItem(mwPlayer);
+        mwPlayer.getPlayer().getInventory().setItem(slot, itemStack);
+    }
+    
+    public static ItemStack getCustomHead(String base64Texture) {
+        ItemStack headItem = new ItemStack(Material.PLAYER_HEAD);
+        SkullMeta skullMeta = (SkullMeta) headItem.getItemMeta();
+        setSkinViaBase64(skullMeta, base64Texture);
+        headItem.setItemMeta(skullMeta);
+        return headItem;
+    }
+
+    /**
+     * A method used to set the skin of a player skull via a base64 encoded string.
+     *
+     * Source: <a href="https://www.spigotmc.org/threads/generated-texture-to-heads.512604/#post-4198463">Post by BoBoBalloon</a>
+     *
+     * @param meta the skull meta to modify
+     * @param base64 the base64 encoded string
+     */
+    private static void setSkinViaBase64(SkullMeta meta, String base64) {
+        try {
+            Method setProfile = meta.getClass().getDeclaredMethod("setProfile", GameProfile.class);
+            setProfile.setAccessible(true);
+
+            GameProfile profile = new GameProfile(UUID.randomUUID(), "skull-texture");
+            profile.getProperties().put("textures", new Property("textures", base64));
+
+            setProfile.invoke(meta, profile);
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+            Logger.ERROR.log("There was a severe internal reflection error when attempting to set the skin of a player skull via base64!");
+            e.printStackTrace();
+        }
+    }
+    
+    private void updatePapiValues(Player player) {
+        finalDisplayName = Messages.getPapiMessage(displayName, player);
+
+        finalLoreList.clear();
+        for (String lore : loreList) {
+            finalLoreList.add(Messages.getPapiMessage(lore, player));
+        }
+    }
+    
+    public static void setEnchantment(ItemStack itemStack) {
+        ItemMeta itemMeta = itemStack.getItemMeta();
+        if (itemMeta != null) itemMeta.addEnchant(Enchantment.LUCK, 10, true);
+        itemStack.setItemMeta(itemMeta);
+    }
+    
+    public static void setDisplayName(ItemStack itemStack, String displayName) {
+        ItemMeta itemMeta = itemStack.getItemMeta();
+        if (itemMeta != null) itemMeta.setDisplayName(displayName);
+        itemStack.setItemMeta(itemMeta);
+    }
+    
+    public static void hideMetaValues(ItemStack itemStack) {
+        ItemMeta itemMeta = itemStack.getItemMeta();
+        if (itemMeta != null) itemMeta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_DESTROYS, ItemFlag.HIDE_DYE, ItemFlag.HIDE_ENCHANTS, 
+                ItemFlag.HIDE_PLACED_ON, ItemFlag.HIDE_UNBREAKABLE, ItemFlag.HIDE_POTION_EFFECTS);
+        itemStack.setItemMeta(itemMeta);
+    }
+    
+    public void setItemRequirement(ConfigurationSection input) {
+        ConfigurationSection cfg = input.getConfigurationSection("view_requirement");
+        if (cfg != null) {
+            this.itemRequirement = new ItemRequirement(cfg);
+            return;
+        }
+        this.itemRequirement = new ItemRequirement();
+    }
+}

+ 55 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuUtils.java

@@ -0,0 +1,55 @@
+package de.butzlabben.missilewars.menus;
+
+import de.butzlabben.missilewars.MissileWars;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.player.MWPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+
+public class MenuUtils {
+    
+    private final Game game;
+
+    public MenuUtils(Game game) {
+        this.game = game;
+    }
+    
+    public Game getGame() {
+        return game;
+    }
+    
+    /**
+     * This method gets the interaction protection variable for a player.
+     *
+     * @param player (Player) the target player
+     */
+    public boolean isInteractDelay(Player player) {
+        MWPlayer mwPlayer = getGame().getPlayer(player);
+        if (mwPlayer == null) return false;
+
+        return mwPlayer.isPlayerInteractEventCancel();
+    }
+    
+    public boolean isInteractDelay(MWPlayer mwPlayer, InventoryClickEvent event) {
+        if (isInteractDelay(mwPlayer.getPlayer())) {
+            event.setCancelled(true);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * This method sets an interaction protection variable for a player for
+     * a short time.
+     *
+     * @param player (Player) the target player
+     */
+    public void setInteractDelay(Player player) {
+        MWPlayer mwPlayer = getGame().getPlayer(player);
+        if (mwPlayer == null) return;
+
+        mwPlayer.setPlayerInteractEventCancel(true);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> mwPlayer.setPlayerInteractEventCancel(false), 10);
+    }
+}

+ 63 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/hotbar/GameJoinMenu.java

@@ -0,0 +1,63 @@
+package de.butzlabben.missilewars.menus.hotbar;
+
+import de.butzlabben.missilewars.Logger;
+import de.butzlabben.missilewars.configuration.Config;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.menus.ItemRequirement;
+import de.butzlabben.missilewars.menus.MenuItem;
+import de.butzlabben.missilewars.player.MWPlayer;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.tuple.Pair;
+import scala.Int;
+
+import java.util.*;
+
+public class GameJoinMenu {
+
+
+    private final MWPlayer mwPlayer;
+    private final Game game;
+    
+    // configured hotbar items:
+    @Getter @Setter public static Map<Integer, Map<Integer, MenuItem>> menuItems;
+    
+    // finale hotbar items based of the current requirement-check:
+    @Getter public Map<Integer, MenuItem> finalMenuItems = new HashMap<>();
+    
+    public GameJoinMenu(MWPlayer mwPlayer) {
+        this.mwPlayer = mwPlayer;
+        this.game = mwPlayer.getGame();
+    }
+    
+    public void getMenu() {
+        if (finalMenuItems != null) finalMenuItems.clear();
+        
+        for (Map<Integer, MenuItem> itemsPerSlot : menuItems.values()) {
+
+            // Convert the keys into a sorted list to sort the priority values:
+            List<Integer> priorityList = new ArrayList<>(itemsPerSlot.keySet());
+            priorityList.sort(Collections.reverseOrder());
+
+
+            // Iterate over the sorted priority values from the biggest to the lowest and check the requirements:
+            for (Integer priority : priorityList) {
+
+                MenuItem item = itemsPerSlot.get(priority);
+                
+                if (item.getItemRequirement().isRequirementsGiven(mwPlayer)) {
+                    // The requirements are fulfilled. Send the final item to the player inventory:
+                    item.sendToPlayer(mwPlayer);
+                    finalMenuItems.put(item.getSlot(), item);
+                    Logger.DEBUG.log("GameJoinMenu: - Slot " + item.getSlot() + ": Item with priority '" + priority 
+                            + "' was added to the inventory menu of " + mwPlayer.getPlayer().getName());
+                    break;
+                }
+            }
+            
+        }
+    }
+    
+}
+
+

+ 201 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/MapVoteMenu.java

@@ -0,0 +1,201 @@
+package de.butzlabben.missilewars.menus.inventory;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.pane.OutlinePane;
+import com.github.stefvanschie.inventoryframework.pane.PaginatedPane;
+import com.github.stefvanschie.inventoryframework.pane.component.PercentageBar;
+import de.butzlabben.missilewars.configuration.Config;
+import de.butzlabben.missilewars.configuration.arena.Arena;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.enums.MapChooseProcedure;
+import de.butzlabben.missilewars.menus.MenuItem;
+import de.butzlabben.missilewars.menus.MenuUtils;
+import de.butzlabben.missilewars.player.MWPlayer;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MapVoteMenu {
+    
+    private final Map<String, Arena> arenaDisplayNames = new HashMap<>();
+    
+    private final MWPlayer mwPlayer;
+    private final Game game;
+    private final MenuUtils menuUtils;
+    ChestGui gui;
+    
+    PaginatedPane paginatedPane;
+    
+    OutlinePane backwards;
+    OutlinePane forwards;
+    GuiItem backwardsItem;
+    GuiItem forwardsItem;
+    
+    final ItemStack backwardsItemActive = MenuItem.getCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDliMmJlZTM5YjZlZjQ3ZTE4MmQ2ZjFkY2E5ZGVhODQyZmNkNjhiZGE5YmFjYzZhNmQ2NmE4ZGNkZjNlYyJ9fX0=");
+    final ItemStack backwardsItemInactive = MenuItem.getCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTQyZmRlOGI4MmU4YzFiOGMyMmIyMjY3OTk4M2ZlMzVjYjc2YTc5Nzc4NDI5YmRhZGFiYzM5N2ZkMTUwNjEifX19");
+    final ItemStack forwardsItemActive = MenuItem.getCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTQxZmY2YmM2N2E0ODEyMzJkMmU2NjllNDNjNGYwODdmOWQyMzA2NjY1YjRmODI5ZmI4Njg5MmQxM2I3MGNhIn19fQ==");
+    final ItemStack forwardsItemInactive = MenuItem.getCustomHead("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDA2MjYyYWYxZDVmNDE0YzU5NzA1NWMyMmUzOWNjZTE0OGU1ZWRiZWM0NTU1OWEyZDZiODhjOGQ2N2I5MmVhNiJ9fX0=");
+    
+    public MapVoteMenu(MWPlayer mwPlayer) {
+        this.mwPlayer = mwPlayer;
+        this.game = mwPlayer.getGame();
+        this.menuUtils = new MenuUtils(game);
+        
+        MenuItem.setDisplayName(backwardsItemActive, Config.MapVoteMenuItems.BACKWARDS_ITEM_ACTIVE.getMessage());
+        MenuItem.setDisplayName(backwardsItemInactive, Config.MapVoteMenuItems.BACKWARDS_ITEM_INACTIVE.getMessage());
+        MenuItem.setDisplayName(forwardsItemActive, Config.MapVoteMenuItems.FORWARDS_ITEM_ACTIVE.getMessage());
+        MenuItem.setDisplayName(forwardsItemInactive, Config.MapVoteMenuItems.FORWARDS_ITEM_INACTIVE.getMessage());
+        
+        for (Arena arena : game.getLobby().getArenas()) {
+            arenaDisplayNames.put(arena.getDisplayName(), arena);
+        }
+        
+        gui = new ChestGui(6, getTitle());
+        paginatedPane = new PaginatedPane(0, 0, 9, 6);
+        
+        gui.addPane(paginatedPane);
+    }
+    
+    public void openMenu() {
+        updateGuiItems();
+        gui.show(mwPlayer.getPlayer());
+    }
+    
+    public static String getTitle() {
+        return Config.getMapVoteMenuTitle();
+    }
+    
+    private void updateGuiItems() {
+        
+        backwards = new OutlinePane(3, 5, 1, 1);
+        forwards = new OutlinePane(5, 5, 1, 1);
+        
+        int maxPages = (int) Math.ceil(game.getLobby().getArenas().size() / 5d);
+        int offset = 0;
+        for (int page = 1; page <= maxPages; page++) {
+            
+            // vertical arena item list for vote:
+            OutlinePane arenas = new OutlinePane(0, 0, 1, 5);
+            
+            PercentageBar voteResultBar;
+            
+            for (int n = 1; n <= 5; n++) {
+                
+                // Are there any other arenas?
+                int nextArenaId = (offset * 5) + n - 1;
+                if (game.getLobby().getArenas().size() < (nextArenaId + 1)) break;
+                
+                Arena arena = game.getLobby().getArenas().get(nextArenaId);
+                
+                // arena item:
+                ItemStack item = new ItemStack(Material.valueOf(arena.getDisplayMaterial().toUpperCase()));
+                MenuItem.hideMetaValues(item);
+                MenuItem.setDisplayName(item, Config.MapVoteMenuItems.MAP_ITEM.getMessage()
+                        .replace("{arena-name}", arena.getDisplayName()));
+                if (game.getMapVoting().isVotedMapOfPlayer(arena, mwPlayer)) MenuItem.setEnchantment(item);
+                
+                arenas.addItem(new GuiItem(item));
+                
+                // vote percent display
+                voteResultBar = new PercentageBar(1, n - 1, 8, 1);
+                voteResultBar.setPercentage((float) (game.getMapVoting().getPercentOf(arena) / 100));
+                
+                ItemStack impactDisplayItem = new ItemStack(Material.YELLOW_STAINED_GLASS_PANE);
+                MenuItem.hideMetaValues(item);
+                MenuItem.setDisplayName(impactDisplayItem, Config.MapVoteMenuItems.VOTE_RESULT_BAR.getMessage()
+                        .replace("{vote-percent}", game.getMapVoting().getPercentOfMsg(arena)));
+                voteResultBar.setFillItem(new GuiItem(impactDisplayItem));
+                
+                ItemStack backgroundItem = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
+                MenuItem.hideMetaValues(backgroundItem);
+                voteResultBar.setBackgroundItem(new GuiItem(backgroundItem));
+                
+                paginatedPane.addPane(page - 1, voteResultBar);
+            }
+            
+            
+            arenas.setOnClick(event -> {
+                // prevent spam with the event handling
+                if (menuUtils.isInteractDelay(mwPlayer, event)) return;
+                menuUtils.setInteractDelay(mwPlayer.getPlayer());
+                
+                String itemName = event.getCurrentItem().getItemMeta().getDisplayName();
+                String arenaName = arenaDisplayNames.get(itemName).getName();
+                mwPlayer.getPlayer().performCommand("mw vote " + arenaName);
+                
+                updateGuiForAllPlayer();
+            });
+            
+            backwards.setOnClick(event -> {
+                if (isFirstPage()) {
+                    event.setCancelled(true);
+                    return;
+                }
+
+                paginatedPane.setPage(paginatedPane.getPage() - 1);
+                backwardsItem.setItem(getBackwardsItem());
+                forwardsItem.setItem(getForwardsItem());
+                gui.update();
+            });
+            
+            forwards.setOnClick(event -> {
+                if (isLastPage()) {
+                    event.setCancelled(true);
+                    return;
+                }
+
+                paginatedPane.setPage(paginatedPane.getPage() + 1);
+                backwardsItem.setItem(getBackwardsItem());
+                forwardsItem.setItem(getForwardsItem());
+                gui.update();
+            });
+            
+            
+            paginatedPane.addPane(page - 1, arenas);
+            paginatedPane.addPane(page - 1, backwards);
+            paginatedPane.addPane(page - 1, forwards);
+            offset++;
+        }
+        
+        backwardsItem = new GuiItem(getBackwardsItem());
+        forwardsItem = new GuiItem(getForwardsItem());
+        
+        backwards.addItem(backwardsItem);
+        forwards.addItem(forwardsItem);
+        
+        gui.update();
+        
+    }
+    
+    private void updateGuiForAllPlayer() {
+        // Update the GUI for all players looking at it:
+        game.getPlayers().forEach((uuid, mwPlayer1) -> mwPlayer1.getMapVoteMenu().updateGuiItems());
+    }
+    
+    private boolean isFirstPage() {
+        return (paginatedPane.getPage() == 0);
+    }
+    
+    private boolean isLastPage() {
+        return ((paginatedPane.getPage() + 1) >= paginatedPane.getPages());
+    }
+    
+    private ItemStack getBackwardsItem() {
+        if (isFirstPage()) {
+            return backwardsItemInactive;
+        } else {
+            return backwardsItemActive;
+        }
+    }
+    
+    private ItemStack getForwardsItem() {
+        if (isLastPage()) {
+            return forwardsItemInactive;
+        } else {
+            return forwardsItemActive;
+        }
+    }
+}

+ 144 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/TeamSelectionMenu.java

@@ -0,0 +1,144 @@
+package de.butzlabben.missilewars.menus.inventory;
+
+import com.github.stefvanschie.inventoryframework.gui.GuiItem;
+import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
+import com.github.stefvanschie.inventoryframework.pane.OutlinePane;
+import de.butzlabben.missilewars.configuration.Config;
+import de.butzlabben.missilewars.game.Game;
+import de.butzlabben.missilewars.game.TeamManager;
+import de.butzlabben.missilewars.menus.MenuItem;
+import de.butzlabben.missilewars.menus.MenuUtils;
+import de.butzlabben.missilewars.player.MWPlayer;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+public class TeamSelectionMenu {
+    
+    private final MWPlayer mwPlayer;
+    private final Game game;
+    private final TeamManager teamManager;
+    private final MenuUtils menuUtils;
+    ChestGui gui;
+    
+    ItemStack item1, item2, itemSpec;
+    ItemStack item1Enchanted, item2Enchanted, itemSpecEnchanted;
+    
+    // A replacement item in the event that the selection is deactivated.
+    ItemStack deactivatedItem;
+    
+    OutlinePane pane1, pane2, paneSpec;
+    
+    public TeamSelectionMenu(MWPlayer mwPlayer) {
+        this.mwPlayer = mwPlayer;
+        this.game = mwPlayer.getGame();
+        this.teamManager = game.getTeamManager();
+        this.menuUtils = new MenuUtils(game);
+        
+        initialItems();
+    }
+    
+    public void initialItems() {
+        
+        deactivatedItem = new ItemStack(Material.LIGHT_GRAY_STAINED_GLASS_PANE);
+        MenuItem.hideMetaValues(deactivatedItem);
+        
+        if (mwPlayer.getPlayer().hasPermission("mw.change.team.player")) {
+            item1 = teamManager.getTeam1().getMenuItem();
+            item2 = teamManager.getTeam2().getMenuItem();
+        } else {
+            item1 = deactivatedItem.clone();
+            item2 = deactivatedItem.clone();
+        }
+        
+        if (mwPlayer.getPlayer().hasPermission("mw.change.team.spectator")) {
+            itemSpec = teamManager.getTeamSpec().getMenuItem();
+        } else {
+            itemSpec = deactivatedItem.clone();
+        }
+        
+        item1Enchanted = item1.clone();
+        item2Enchanted = item2.clone();
+        itemSpecEnchanted = itemSpec.clone();
+        
+        MenuItem.setEnchantment(item1Enchanted);
+        MenuItem.setEnchantment(item2Enchanted);
+        MenuItem.setEnchantment(itemSpecEnchanted);
+    }
+    
+    public void openMenu() {
+        gui = new ChestGui(3, getTitle());
+        
+        pane1 = new OutlinePane(2, 1, 1, 1);
+        pane2 = new OutlinePane(6, 1, 1, 1);
+        paneSpec = new OutlinePane(4, 1, 1, 1);
+        
+        updateGuiItems(mwPlayer);
+        
+        pane1.setOnClick(event -> {
+            // prevent spam with the event handling
+            if (menuUtils.isInteractDelay(mwPlayer, event)) return;
+            menuUtils.setInteractDelay(mwPlayer.getPlayer());
+            
+            mwPlayer.getPlayer().performCommand("mw change 1");
+            
+            updateGuiItems(mwPlayer);
+            gui.update();
+        });
+        
+        pane2.setOnClick(event -> {
+            // prevent spam with the event handling
+            if (menuUtils.isInteractDelay(mwPlayer, event)) return;
+            menuUtils.setInteractDelay(mwPlayer.getPlayer());
+            
+            mwPlayer.getPlayer().performCommand("mw change 2");
+            
+            updateGuiItems(mwPlayer);
+            gui.update();
+        });
+        
+        paneSpec.setOnClick(event -> {
+            // prevent spam with the event handling
+            if (menuUtils.isInteractDelay(mwPlayer, event)) return;
+            menuUtils.setInteractDelay(mwPlayer.getPlayer());
+            
+            mwPlayer.getPlayer().performCommand("mw change spec");
+            
+            updateGuiItems(mwPlayer);
+            gui.update();
+        });
+        
+        
+        gui.addPane(pane1);
+        gui.addPane(pane2);
+        gui.addPane(paneSpec);
+        
+        gui.show(mwPlayer.getPlayer());
+    }
+    
+    public static String getTitle() {
+        return Config.getTeamSelectionMenuTitle();
+    }
+    
+    private void updateGuiItems(MWPlayer mwPlayer) {
+
+        if (!pane1.getItems().isEmpty()) pane1.removeItem(pane1.getItems().get(0));
+        if (!pane2.getItems().isEmpty()) pane2.removeItem(pane2.getItems().get(0));
+        if (!paneSpec.getItems().isEmpty()) paneSpec.removeItem(paneSpec.getItems().get(0));
+        
+        if (mwPlayer.getTeam() == teamManager.getTeam1()) {
+            pane1.addItem(new GuiItem(item1Enchanted));
+            pane2.addItem(new GuiItem(item2));
+            paneSpec.addItem(new GuiItem(itemSpec));
+        } else if (mwPlayer.getTeam() == teamManager.getTeam2()) {
+            pane1.addItem(new GuiItem(item1));
+            pane2.addItem(new GuiItem(item2Enchanted));
+            paneSpec.addItem(new GuiItem(itemSpec));
+        } else if (mwPlayer.getTeam() == teamManager.getTeamSpec()) {
+            pane1.addItem(new GuiItem(item1));
+            pane2.addItem(new GuiItem(item2));
+            paneSpec.addItem(new GuiItem(itemSpecEnchanted));
+        }
+    }
+    
+}

+ 28 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/player/MWPlayer.java

@@ -18,9 +18,13 @@
 
 package de.butzlabben.missilewars.player;
 
+import de.butzlabben.missilewars.configuration.Config;
 import de.butzlabben.missilewars.game.Game;
 import de.butzlabben.missilewars.game.Team;
 import de.butzlabben.missilewars.game.equipment.PlayerEquipmentRandomizer;
+import de.butzlabben.missilewars.menus.hotbar.GameJoinMenu;
+import de.butzlabben.missilewars.menus.inventory.MapVoteMenu;
+import de.butzlabben.missilewars.menus.inventory.TeamSelectionMenu;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
 import lombok.Setter;
@@ -47,10 +51,20 @@ public class MWPlayer implements Runnable {
     private PlayerEquipmentRandomizer playerEquipmentRandomizer;
     @Setter
     private boolean playerInteractEventCancel = false;
+    private GameJoinMenu gameJoinMenu;
+    private MapVoteMenu mapVoteMenu;
+    private TeamSelectionMenu teamSelectionMenu;
+    private long lastTeamChangeTime;
 
     public MWPlayer(Player player, Game game) {
         this.uuid = player.getUniqueId();
         this.game = game;
+        
+        this.gameJoinMenu = new GameJoinMenu(this);
+        this.mapVoteMenu = new MapVoteMenu(this);
+        this.teamSelectionMenu = new TeamSelectionMenu(this);
+        
+        setLastTeamChangeTime();
     }
 
     public Player getPlayer() {
@@ -70,4 +84,18 @@ public class MWPlayer implements Runnable {
     public String toString() {
         return "MWPlayer(uuid=" + uuid + ", id=" + id + ", teamName=" + getTeam().getName() + ")";
     }
+
+    public void setLastTeamChangeTime() {
+        this.lastTeamChangeTime = System.currentTimeMillis();
+    }
+
+    public long getWaitTimeForTeamChange() {
+        // anti-spam intervall in seconds
+        int antiSpamTime = Config.getTeamChangeCmdIntervall();
+        
+        long currentTime = System.currentTimeMillis();
+        
+        // Output is in seconds.
+        return (antiSpamTime - ((currentTime - lastTeamChangeTime) / 1000));
+    }
 }