ソースを参照

Merge pull request #127 from RedstoneFuture/feature/game-join-menu

Full improvement of Game-Join/Leave process, Adding GUIs, Reworking of player-management, Refactoring, Adding commands
RedstoneFuture 1 年間 前
コミット
6a8b4889e4
43 ファイル変更2560 行追加653 行削除
  1. 2 2
      1_13/pom.xml
  2. 7 0
      1_16_FAWE/pom.xml
  3. 8 3
      missilewars-plugin/pom.xml
  4. 16 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/MWCommandCompletions.java
  5. 18 10
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/MWCommands.java
  6. 157 31
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/UserCommands.java
  7. 98 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/ActionSet.java
  8. 238 60
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Config.java
  9. 59 10
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Messages.java
  10. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/arena/Arena.java
  11. 49 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/GameTeamConfiguration.java
  12. 11 14
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/Lobby.java
  13. 105 282
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java
  14. 268 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameJoinManager.java
  15. 138 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameLeaveManager.java
  16. 1 1
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/GameManager.java
  17. 17 8
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/MapVoting.java
  18. 44 30
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Team.java
  19. 179 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/TeamManager.java
  20. 30 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/JoinIngameBehavior.java
  21. 31 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/RejoinIngameBehavior.java
  22. 29 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/TeamType.java
  23. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/equipment/EquipmentManager.java
  24. 64 11
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MissileWarsPlaceholder.java
  25. 3 5
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MotdManager.java
  26. 6 25
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/ScoreboardManager.java
  27. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/signs/MWSign.java
  28. 3 3
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/stats/FightStats.java
  29. 7 11
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java
  30. 0 77
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/VoteInventory.java
  31. 2 2
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/PlayerListener.java
  32. 57 6
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/EndListener.java
  33. 1 1
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/GameBoundListener.java
  34. 73 14
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/GameListener.java
  35. 68 36
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/LobbyListener.java
  36. 111 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/ItemRequirement.java
  37. 150 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuItem.java
  38. 55 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/MenuUtils.java
  39. 63 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/hotbar/GameJoinMenu.java
  40. 201 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/MapVoteMenu.java
  41. 144 0
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/menus/inventory/TeamSelectionMenu.java
  42. 35 4
      missilewars-plugin/src/main/java/de/butzlabben/missilewars/player/MWPlayer.java
  43. 6 1
      pom.xml

+ 2 - 2
1_13/pom.xml

@@ -31,8 +31,8 @@
     <artifactId>1_13</artifactId>
 
     <dependencies>
+        <!-- WorldEdit (Bukkit) API -->
         <dependency>
-            <!-- WorldEdit (Bukkit) API -->
             <groupId>com.sk89q.worldedit</groupId>
             <artifactId>worldedit-bukkit</artifactId>
             <version>7.0.0-SNAPSHOT</version>
@@ -46,8 +46,8 @@
             </exclusions>
         </dependency>
 
+        <!-- WorldEdit (Core) API -->
         <dependency>
-            <!-- WorldEdit (Core) API -->
             <groupId>com.sk89q.worldedit</groupId>
             <artifactId>worldedit-core</artifactId>
             <version>7.0.0-SNAPSHOT</version>

+ 7 - 0
1_16_FAWE/pom.xml

@@ -38,6 +38,13 @@
             <version>2.0.0-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
+
+        <!-- Adventure-MiniMessage API for FAWE -->
+        <dependency>
+            <groupId>net.kyori</groupId>
+            <artifactId>adventure-text-minimessage</artifactId>
+            <version>4.17.0</version>
+        </dependency>
     </dependencies>
 
 </project>

+ 8 - 3
missilewars-plugin/pom.xml

@@ -26,7 +26,7 @@
         <version>1.0</version>
     </parent>
 
-    <version>4.6.1</version>
+    <version>4.7.0</version>
 
     <modelVersion>4.0.0</modelVersion>
 
@@ -117,7 +117,7 @@
         <dependency>
             <groupId>com.mojang</groupId>
             <artifactId>authlib</artifactId>
-            <version>1.5.21</version>
+            <version>1.5.25</version>
             <scope>provided</scope>
         </dependency>
 
@@ -129,13 +129,18 @@
             <scope>provided</scope>
         </dependency>
 
-
         <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
             <version>4.13.2</version>
             <scope>test</scope>
         </dependency>
+        
+        <dependency>
+            <groupId>com.github.stefvanschie.inventoryframework</groupId>
+            <artifactId>IF</artifactId>
+            <version>0.10.13</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 16 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/MWCommandCompletions.java

@@ -21,6 +21,7 @@ package de.butzlabben.missilewars.commands;
 import co.aikar.commands.BukkitCommandCompletionContext;
 import co.aikar.commands.CommandCompletions;
 import co.aikar.commands.PaperCommandManager;
+import com.google.common.collect.ImmutableList;
 import de.butzlabben.missilewars.game.Game;
 import de.butzlabben.missilewars.game.GameManager;
 import org.bukkit.command.CommandSender;
@@ -36,6 +37,7 @@ public class MWCommandCompletions {
         registerGamesResult();
         registerMissilesResult();
         registerArenasResult();
+        registerTeamsResult();
     }
 
     private void registerGamesResult() {
@@ -69,5 +71,19 @@ public class MWCommandCompletions {
             return game.getLobby().getPossibleArenas();
         });
     }
+    
+    private void registerTeamsResult() {
+        commandCompletions.registerCompletion("teams", c -> {
+            CommandSender sender = c.getSender();
+
+            if (!(sender instanceof Player)) return null;
+            Player player = (Player) sender;
+
+            Game game = GameManager.getInstance().getGame(player.getLocation());
+            if (game == null) return null;
+
+            return ImmutableList.of("1", "2", "spec");
+        });
+    }
 
 }

+ 18 - 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,8 +44,10 @@ public class MWCommands extends BaseCommand {
     @Description("Shows information about the MissileWars Plugin.")
     public void mwCommand(CommandSender sender) {
 
+        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", "/mw change <1|2>", "Changes your team.");
+        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.");
 
         sendHelpMessage(sender, "mw.stats", "/mw stats [from] [arena]", "Shows stats.");
@@ -61,12 +64,13 @@ public class MWCommands extends BaseCommand {
         sendHelpMessage(sender, "mw.debug", "/mw debug", "Show debug info.");
         sendHelpMessage(sender, "mw.restartall", "/mw restartall", "Restart all games.");
 
-        sendHelpMessage(sender, "/mw version", "Show the plugin version.");
+        sendHelpMessage(sender, "mw.version", "/mw version", "Show the plugin version.");
         sendHelpMessage(sender, "mw.setup", "/mw setup <main|lobby|arena> ...", "Setup the MW locations or the lobby/arena locations.");
     }
     
     @Subcommand("version")
     @CommandCompletion("@nothing")
+    @CommandPermission("mw.version")
     public void versionCommand(CommandSender sender, String[] args) {
         
         sender.sendMessage(Messages.getPrefix() + "Installed version: " + MissileWars.getInstance().version + " by RedstoneFuture & Butzlabben");
@@ -78,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");
         }
 
     }
@@ -205,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();
     }
 

+ 157 - 31
missilewars-plugin/src/main/java/de/butzlabben/missilewars/commands/UserCommands.java

@@ -29,13 +29,14 @@ import de.butzlabben.missilewars.game.Game;
 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.player.MWPlayer;
 import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 
 @CommandAlias("mw|missilewars")
 public class UserCommands extends BaseCommand {
-
+    
     @Subcommand("vote")
     @CommandCompletion("@arenas")
     @CommandPermission("mw.vote")
@@ -60,12 +61,53 @@ public class UserCommands extends BaseCommand {
             return;
         }
         
+        if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_CANT_VOTE));
+            return;
+        }
+        
         game.getMapVoting().addVote(player, args[0]);
     }
+    
+    @Subcommand("mapmenu|votegui")
+    @CommandCompletion("@nothing")
+    @CommandPermission("mw.mapmenu")
+    public void mapmenuCommand(CommandSender sender, String[] args) {
+
+        if (!MWCommands.senderIsPlayer(sender)) return;
+        Player player = (Player) sender;
+
+        if (args.length > 0) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_TO_MANY_ARGUMENTS));
+            return;
+        }
+        
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_IN_GAME_AREA));
+            return;
+        }
+        
+        if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_CANT_VOTE));
+            return;
+        }
+        
+        // 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();
+    }
 
-    @Subcommand("change")
-    @CommandCompletion("@range:1-2")
-    @CommandPermission("mw.change")
+    @Subcommand("change|switch|team")
+    @CommandCompletion("@teams")
+    @CommandPermission("mw.change.use")
     public void changeCommand(CommandSender sender, String[] args) {
 
         if (!MWCommands.senderIsPlayer(sender)) return;
@@ -86,46 +128,130 @@ public class UserCommands extends BaseCommand {
             player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_IN_GAME_AREA));
             return;
         }
-
-        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;
+            }
+            
+            // 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;
+            
         }
-
-        // too late for team change:
-        if (game.getTaskManager().getTimer().getSeconds() < 10) {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_CHANGE_TEAM_NO_LONGER_NOW));
-            return;
-        }
-
-        if (!(args[0].equalsIgnoreCase("1") || args[0].equalsIgnoreCase("2"))) {
-            sender.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_INVALID_TEAM_NUMBER));
-            return;
+        
+        
+        Team from = mwPlayer.getTeam();
+        Team to;
+        
+        switch (args[0]) {
+            case "1":
+            case "team1":
+                if (!player.hasPermission("mw.change.team.player")) {
+                    Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
+                    return;
+                }
+                to = game.getTeamManager().getTeam1();
+                break;
+            case "2":
+            case "team2":
+                if (!player.hasPermission("mw.change.team.player")) {
+                    Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
+                    return;
+                }
+                to = game.getTeamManager().getTeam2();
+                break;
+            case "spec":
+            case "spectator":
+                if (!player.hasPermission("mw.change.team.spectator")) {
+                    Messages.getMessage(true, Messages.MessageEnum.NO_PERMISSION);
+                    return;
+                }
+                to = game.getTeamManager().getTeamSpec();
+                break;
+            default:
+                sender.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_INVALID_TEAM));
+                return;
         }
-
-        MWPlayer mwPlayer = game.getPlayer(player);
-        int teamNumber = Integer.parseInt(args[0]);
-        Team to = teamNumber == 1 ? game.getTeam1() : game.getTeam2();
-
+        
         // Is the same team?
-        if (to == mwPlayer.getTeam()) {
+        if (from == to) {
             player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_ALREADY_IN_TEAM));
             return;
         }
-
+        
         // Would the number of team members be too far apart?
-        if (!game.isValidTeamSwitch(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;
+            }
+        }
+        
+        // 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 {
+            if (game.areTooManyPlayers()) {
+                player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_PLAYER_MAX_REACHED));
+                return;
+            }
         }
+        
+        game.getGameJoinManager().runPlayerTeamSwitch(mwPlayer, to);
+    }
+    
+    @Subcommand("teammenu|changegui")
+    @CommandCompletion("@nothing")
+    @CommandPermission("mw.teammenu")
+    public void teammenuCommand(CommandSender sender, String[] args) {
 
-        // Remove the player from the old team and add him to the new team
-        to.addMember(mwPlayer);
+        if (!MWCommands.senderIsPlayer(sender)) return;
+        Player player = (Player) sender;
 
-        player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.TEAM_TEAM_CHANGED).replace("%team%", to.getFullname()));
-        game.getScoreboardManager().updateScoreboard();
+        if (args.length > 0) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.COMMAND_TO_MANY_ARGUMENTS));
+            return;
+        }
+        
+        Game game = GameManager.getInstance().getGame(player.getLocation());
+        if (game == null) {
+            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.GAME_NOT_IN_GAME_AREA));
+            return;
+        }
+        
+        // The GUI can also be opened if the settings indicate that it is too late to change teams.
+        
+        MWPlayer mwPlayer = game.getPlayer(player);
+        mwPlayer.getTeamSelectionMenu().openMenu();
     }
-
+    
     @Subcommand("quit|leave")
     @CommandCompletion("@nothing")
     @CommandPermission("mw.quit")

+ 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;
+            }
+            
+        });
+        
+    }
+}

+ 238 - 60
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Config.java

@@ -21,17 +21,15 @@ package de.butzlabben.missilewars.configuration;
 import de.butzlabben.missilewars.Logger;
 import de.butzlabben.missilewars.MissileWars;
 import de.butzlabben.missilewars.game.GameManager;
+import de.butzlabben.missilewars.menus.MenuItem;
 import de.butzlabben.missilewars.util.SetupUtil;
-import org.bukkit.Bukkit;
-import org.bukkit.Location;
-import org.bukkit.Material;
-import org.bukkit.World;
+import lombok.Getter;
+import org.bukkit.*;
 import org.bukkit.configuration.ConfigurationSection;
 import org.bukkit.configuration.file.YamlConfiguration;
 
 import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
 
 import static org.bukkit.Material.JUKEBOX;
 import static org.bukkit.Material.valueOf;
@@ -74,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);
@@ -122,56 +122,115 @@ public class Config {
         cfg.addDefault("sidebar.member_list_max", 4);
 
         if (isNewConfig) {
-            List<String> sidebarList = new ArrayList<>();
-
-            sidebarList.add("&7Time left:");
-            sidebarList.add("&e» %time%m");
-            sidebarList.add("");
-            sidebarList.add("%team1% &7» %team1_color%%team1_amount%");
-            sidebarList.add("");
-            sidebarList.add("%team2% &7» %team2_color%%team2_amount%");
-
-            cfg.set("sidebar.entries", sidebarList);
-        }
-    }
-
-    public static List<String> getScoreboardEntries() {
-        return cfg.getStringList("sidebar.entries");
-    }
-
-    /**
-     * This method gets the minecraft material type of the block to start missiles.
-     */
-    public static Material getStartReplace() {
-        String name = cfg.getString("replace.material").toUpperCase();
-        try {
-            return valueOf(name);
-        } catch (Exception e) {
-            Logger.WARN.log("Could not use " + name + " as start material!");
+            
+            cfg.set("sidebar.entries", new ArrayList<String>() {{
+                add("&7Time left:");
+                add("&e» %time%m");
+                add("");
+                add("%team1% &7» %team1_color%%team1_amount%");
+                add("");
+                add("%team2% &7» %team2_color%%team2_amount%");
+            }});
         }
-        return null;
-    }
 
-    public static Location getFallbackSpawn() {
-        ConfigurationSection cfg = Config.cfg.getConfigurationSection("fallback_spawn");
-        World world = Bukkit.getWorld(cfg.getString("world"));
-        if (world == null) {
-            Logger.WARN.log("The world configured at \"fallback_location.world\" couldn't be found. Using the default one");
-            world = Bukkit.getWorlds().get(0);
-        }
-        Location location = new Location(world,
-                cfg.getDouble("x"),
-                cfg.getDouble("y"),
-                cfg.getDouble("z"),
-                (float) cfg.getDouble("yaw"),
-                (float) cfg.getDouble("pitch"));
-        if (GameManager.getInstance().getGame(location) != null) {
-            Logger.WARN.log("Your fallback spawn is inside a game area. This plugins functionality can no longer be guaranteed");
+        String gameJoinMenu = "menus.hotbar_menu.game_join_menu";
+        
+        if (isNewConfig) {
+            
+            // team-selection menu link:
+            
+            cfg.addDefault(gameJoinMenu + ".items.team_selection.display_name", "&eTeam Selection");
+            cfg.addDefault(gameJoinMenu + ".items.team_selection.material", "{player-team-item}");
+            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_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 (A: Map-Vote active):
+            
+            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_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");
+            }});
+            
+            
+            // area info item:
+            
+            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%");
+                add("&e> &fArena: &7%missilewars_arena_displayname_this%");
+                add("&e> &fGame-Time: &7%missilewars_lobby_gameduration_this% min");
+                add("&e> &fMissiles: &7%missilewars_arena_missileamount_this%x");
+                add("&e> &fArena-Size: &7%missilewars_lobby_arenasize_X_this% x %missilewars_lobby_arenasize_Z_this% blocks");
+            }});
+            
+            cfg.set(gameJoinMenu + ".items.areaInfo.left_click_actions", new ArrayList<String>());
+            cfg.set(gameJoinMenu + ".items.areaInfo.right_click_actions", new ArrayList<String>());
         }
-
-        return location;
-    }
-
+        
+        
+        String teamSelectionMenu = "menus.inventory_menu.team_selection_menu";
+        cfg.addDefault(teamSelectionMenu + ".title", "&eTeam Selection Menu");
+        cfg.addDefault(teamSelectionMenu + ".team_item", "{player-team-name}");
+        
+        String mapVoteMenu = "menus.inventory_menu.map_vote_menu";
+        cfg.addDefault(mapVoteMenu + ".title", "&eMap Vote Menu");
+        cfg.addDefault(mapVoteMenu + ".map_item", "&e{arena-name}");
+        cfg.addDefault(mapVoteMenu + ".vote_result_bar", "&7{vote-percent}%");
+        cfg.addDefault(mapVoteMenu + ".navigation.backwards_item.active", "&eprevious page");
+        cfg.addDefault(mapVoteMenu + ".navigation.backwards_item.inactive", "&7previous page");
+        cfg.addDefault(mapVoteMenu + ".navigation.forwards_item.active", "&enext page");
+        cfg.addDefault(mapVoteMenu + ".navigation.forwards_item.inactive", "&7next page");
+    }
+    
     public static void setFallbackSpawn(Location spawnLocation) {
         cfg.set("fallback_spawn.world", spawnLocation.getWorld().getName());
         cfg.set("fallback_spawn.x", spawnLocation.getX());
@@ -183,7 +242,7 @@ public class Config {
         // re-save the config with only validated options
         SetupUtil.safeFile(FILE, cfg);
     }
-
+    
     public static YamlConfiguration getConfig() {
         return cfg;
     }
@@ -195,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");
@@ -231,6 +294,19 @@ public class Config {
     public static String getShieldsFolder() {
         return cfg.getString("shields.folder");
     }
+    
+    /**
+     * This method gets the minecraft material type of the block to start missiles.
+     */
+    public static Material getStartReplace() {
+        String name = cfg.getString("replace.material").toUpperCase();
+        try {
+            return valueOf(name);
+        } catch (Exception e) {
+            Logger.WARN.log("Could not use " + name + " as start material!");
+        }
+        return null;
+    }
 
     public static int getReplaceTicks() {
         return cfg.getInt("replace.after_ticks");
@@ -241,15 +317,15 @@ public class Config {
     }
 
     public static String motdEnded() {
-        return cfg.getString("motd.ended");
+        return Messages.getConvertedMsg(cfg.getString("motd.ended"));
     }
 
     public static String motdGame() {
-        return cfg.getString("motd.ingame");
+        return Messages.getConvertedMsg(cfg.getString("motd.ingame"));
     }
 
     public static String motdLobby() {
-        return cfg.getString("motd.lobby");
+        return Messages.getConvertedMsg(cfg.getString("motd.lobby"));
     }
 
     public static boolean motdEnabled() {
@@ -263,7 +339,27 @@ public class Config {
     public static boolean isShowRealSkins() {
         return cfg.getBoolean("fightstats.show_real_skins");
     }
+    
+    public static Location getFallbackSpawn() {
+        ConfigurationSection cfg = Config.cfg.getConfigurationSection("fallback_spawn");
+        World world = Bukkit.getWorld(cfg.getString("world"));
+        if (world == null) {
+            Logger.WARN.log("The world configured at \"fallback_location.world\" couldn't be found. Using the default one");
+            world = Bukkit.getWorlds().get(0);
+        }
+        Location location = new Location(world,
+                cfg.getDouble("x"),
+                cfg.getDouble("y"),
+                cfg.getDouble("z"),
+                (float) cfg.getDouble("yaw"),
+                (float) cfg.getDouble("pitch"));
+        if (GameManager.getInstance().getGame(location) != null) {
+            Logger.WARN.log("Your fallback spawn is inside a game area. This plugins functionality can no longer be guaranteed");
+        }
 
+        return location;
+    }
+    
     public static String getHost() {
         return cfg.getString("mysql.host");
     }
@@ -293,15 +389,97 @@ public class Config {
     }
 
     public static String getScoreboardTitle() {
-        return cfg.getString("sidebar.title");
+        return Messages.getConvertedMsg(cfg.getString("sidebar.title"));
     }
 
     public static String getScoreboardMembersStyle() {
-        return cfg.getString("sidebar.member_list_style");
+        return Messages.getConvertedMsg(cfg.getString("sidebar.member_list_style"));
     }
 
     public static int getScoreboardMembersMax() {
         return cfg.getInt("sidebar.member_list_max");
     }
-
+    
+    public static List<String> getScoreboardEntries() {
+        return Messages.getConvertedMsgList(cfg.getStringList("sidebar.entries"));
+    }
+    
+    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";
+        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(cfg.getInt("slot"), cfg.getInt("priority"));
+
+            menuItem.setDisplayName(Messages.getConvertedMsg(cfg.getString("display_name")));
+            menuItem.setMaterialName(cfg.getString("material"));
+            menuItem.setItemRequirement(cfg);
+            menuItem.setLoreList(Messages.getConvertedMsgList(cfg.getStringList("lore")));
+            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<>();
+            
+            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;
+    }
+    
+    public static String getTeamSelectionMenuTitle() {
+        return Messages.getConvertedMsg(cfg.getString("menus.inventory_menu.team_selection_menu.title"));
+    }
+    
+    @Getter
+    public enum TeamSelectionMenuItems {
+        TEAM_ITEM("menus.inventory_menu.team_selection_menu.team_item");
+        
+        private final String path;
+
+        TeamSelectionMenuItems(String path) {
+            this.path = path;
+        }
+        
+        public String getMessage() {
+            return Messages.getConvertedMsg(cfg.getString(getPath()));
+        }
+    }
+    
+    public static String getMapVoteMenuTitle() {
+        return Messages.getConvertedMsg(cfg.getString("menus.inventory_menu.map_vote_menu.title"));
+    }
+    
+    @Getter
+    public enum MapVoteMenuItems {
+        MAP_ITEM("menus.inventory_menu.map_vote_menu.map_item"),
+        VOTE_RESULT_BAR("menus.inventory_menu.map_vote_menu.vote_result_bar"),
+        BACKWARDS_ITEM_ACTIVE("menus.inventory_menu.map_vote_menu.navigation.backwards_item.active"),
+        BACKWARDS_ITEM_INACTIVE("menus.inventory_menu.map_vote_menu.navigation.backwards_item.inactive"),
+        FORWARDS_ITEM_ACTIVE("menus.inventory_menu.map_vote_menu.navigation.forwards_item.active"),
+        FORWARDS_ITEM_INACTIVE("menus.inventory_menu.map_vote_menu.navigation.forwards_item.inactive");
+        
+        private final String path;
+
+        MapVoteMenuItems(String path) {
+            this.path = path;
+        }
+        
+        public String getMessage() {
+            return Messages.getConvertedMsg(cfg.getString(getPath()));
+        }
+    }
+    
 }

+ 59 - 10
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Messages.java

@@ -21,10 +21,14 @@ package de.butzlabben.missilewars.configuration;
 import de.butzlabben.missilewars.MissileWars;
 import de.butzlabben.missilewars.util.SetupUtil;
 import lombok.Getter;
+import me.clip.placeholderapi.PlaceholderAPI;
 import org.bukkit.ChatColor;
 import org.bukkit.configuration.file.YamlConfiguration;
+import org.bukkit.entity.Player;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 
 
 /**
@@ -73,15 +77,23 @@ public class Messages {
         return getConfigMessage(MessageEnum.PREFIX);
     }
 
+    /**
+     * This method returns the desired message from the 'messages.yml'. 
+     * The legacy color code with '&' is used.
+     * 
+     * @param msg the target message registered in the 'MessageEnum'
+     * @return (String) the converted message
+     */
     private static String getConfigMessage(MessageEnum msg) {
-        return ChatColor.translateAlternateColorCodes('&', cfg.getString(msg.getPath(),
+        return getConvertedMsg(cfg.getString(msg.getPath(),
                 "&cError while reading from messages.yml: '" + msg.getPath() + "'"));
     }
 
     @Getter
     public enum MessageEnum {
         PREFIX("prefix", "&6•&e● MissileWars &8▎ &7"),
-
+        NO_PERMISSION("no_permission", "You down't have the permission for this."),
+        
         DEBUG_RELOAD_CONFIG("debug.reload_config", "&7Reloaded configs."),
         DEBUG_RESTART_ALL_GAMES_WARN("debug.restart_all_games_warn", "&cWarning: Restarting all games. This may take a while."),
         DEBUG_RESTART_ALL_GAMES("debug.restart_all_games", "&7Restarted all games."),
@@ -98,7 +110,8 @@ public class Messages {
         COMMAND_INVALID_SHIELD("command.invalid_shield", "&cThe specified shield %input% was not found."),
         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_NUMBER("command.invalid_team_number", "&cThe team number is invalid. Use \"1\" or \"2\" to specify the target team."),
+        COMMAND_INVALID_TEAM("command.invalid_team", "&cThe team selection is 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 being able to change the team 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)."),
@@ -107,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."),
@@ -120,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."),
@@ -196,5 +214,36 @@ public class Messages {
         }
 
     }
-
+    
+    /**
+     * This method returns the desired message. Legacy 
+     * color-codes with '&' will be converted to the 
+     * final text message.
+     * 
+     * @param message the target message
+     * @return (String) the converted message
+     */
+    public static String getConvertedMsg(String message) {
+        return ChatColor.translateAlternateColorCodes('&', message);
+    }
+    
+    /**
+     * This method returns the desired message list. 
+     * Legacy color-codes with '&' will be converted 
+     * to the final text message.
+     * 
+     * @param messageList the target message list
+     * @return (String) the converted message list
+     */
+    public static List<String> getConvertedMsgList(List<String> messageList) {
+        List<String> convertedMsgList = new ArrayList<>();
+        for (String message : messageList) {
+            convertedMsgList.add(getConvertedMsg(message));
+        }
+        return convertedMsgList;
+    }
+    
+    public static String getPapiMessage(String message, Player player) {
+        return ChatColor.translateAlternateColorCodes('&', PlaceholderAPI.setPlaceholders(player, message));
+    }
 }

+ 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;
+    }
+}

+ 11 - 14
missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/Lobby.java → missilewars-plugin/src/main/java/de/butzlabben/missilewars/configuration/lobby/Lobby.java

@@ -16,22 +16,18 @@
  * along with MissileWars.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-package de.butzlabben.missilewars.configuration;
+package de.butzlabben.missilewars.configuration.lobby;
 
 import com.google.gson.annotations.SerializedName;
 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");
     }};

+ 105 - 282
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/Game.java

@@ -21,17 +21,16 @@ 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.Lobby;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.configuration.arena.Arena;
+import de.butzlabben.missilewars.configuration.lobby.Lobby;
 import de.butzlabben.missilewars.event.GameStartEvent;
 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.equipment.PlayerEquipmentRandomizer;
 import de.butzlabben.missilewars.game.misc.MotdManager;
 import de.butzlabben.missilewars.game.misc.ScoreboardManager;
 import de.butzlabben.missilewars.game.schematics.SchematicFacing;
@@ -43,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;
@@ -60,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;
 
 /**
@@ -78,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<>();
@@ -88,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;
@@ -98,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;
@@ -129,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();
@@ -149,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");
@@ -157,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));
@@ -178,7 +170,7 @@ public class Game {
                 prepareGame();
             } else {
                 mapVoting.startVote();
-                scoreboardManager.resetScoreboard();
+                updateGameInfo();
             }
         }
 
@@ -194,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();
@@ -253,7 +242,7 @@ public class Game {
 
         timestart = System.currentTimeMillis();
 
-        applyForAllPlayers(this::startForPlayer);
+        applyForAllPlayers(player -> gameJoinManager.startForPlayer(player, true));
 
         updateMOTD();
 
@@ -319,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);
@@ -549,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());
     }
@@ -565,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());
     }
 
@@ -576,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, ...).
      *
@@ -702,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();
@@ -730,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();
 
         }
 
@@ -765,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);
             }
-
+            
         }
     }
 
@@ -798,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 {
@@ -821,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) {

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

@@ -0,0 +1,268 @@
+/*
+ * 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;
+
+@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 isGameJoin) {
+        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, isGameJoin), 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 isGameJoin) {
+        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 ((isGameJoin) && (game.getState() == GameState.INGAME)) {
+                if (!player.hasPermission("mw.teammenu")) return;
+                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);
+    }
+    
+}

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

@@ -21,7 +21,7 @@ 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.Lobby;
+import de.butzlabben.missilewars.configuration.lobby.Lobby;
 import de.butzlabben.missilewars.configuration.Messages;
 import de.butzlabben.missilewars.util.geometry.GameArea;
 import de.butzlabben.missilewars.util.serialization.Serializer;

+ 17 - 8
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/MapVoting.java

@@ -51,11 +51,6 @@ public class MapVoting {
      */
     public void addVote(Player player, String arenaName) {
         
-        if (game.getLobby().getMapChooseProcedure() != MapChooseProcedure.MAPVOTING) {
-            player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_CANT_VOTE));
-            return;
-        }
-        
         if (state == VoteState.NULL) {
             player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_CHANGE_TEAM_NOT_NOW));
             return;
@@ -86,11 +81,11 @@ public class MapVoting {
                 return;
             }
 
-            // remove old vote
+            // remove the old vote
             arenaVotes.remove(mwPlayer);
         }
 
-        // add new vote
+        // add the new vote
         arenaVotes.put(mwPlayer, arena);
 
         player.sendMessage(Messages.getMessage(true, Messages.MessageEnum.VOTE_SUCCESS).replace("%map%", arena.getDisplayName()));
@@ -114,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.
      */
@@ -167,4 +172,8 @@ public class MapVoting {
         game.prepareGame();
     }
     
+    public boolean isVotedMapOfPlayer(Arena arena, MWPlayer mwPlayer) {
+        return (arenaVotes.get(mwPlayer) == arena);
+    }
+    
 }

+ 44 - 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
@@ -51,22 +55,25 @@ public class Team {
     private final String name;
     private final String color;
     private final Game game;
+    private final transient TeamType teamType;
     private final transient ArrayList<MWPlayer> members = new ArrayList<>();
     @Setter private Location spawn;
     @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;
 
@@ -81,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");
@@ -97,6 +100,7 @@ public class Team {
         members.add(mwPlayer);
         mwPlayer.setTeam(this);
         player.setDisplayName(getColorCode() + player.getName() + "§r");
+        
         player.getInventory().setArmorContents(getTeamArmor());
     }
 
@@ -113,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);
@@ -142,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) {
@@ -236,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
+}

+ 29 - 0
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/enums/TeamType.java

@@ -0,0 +1,29 @@
+/*
+ * 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 TeamType {
+    
+    PLAYER,
+    SPECTATOR
+}

+ 2 - 2
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/equipment/EquipmentManager.java

@@ -57,7 +57,7 @@ public class EquipmentManager {
      */
     public void createGameItems() {
 
-        // Will it be used ?
+        // Will it be used?
         if (game.getArena().getSpawn().isSendBow() || game.getArena().getRespawn().isSendBow()) {
 
             ItemStack bow = new ItemStack(Material.BOW);
@@ -71,7 +71,7 @@ public class EquipmentManager {
             this.customBow = bow;
         }
 
-        // Will it be used ?
+        // Will it be used?
         if (game.getArena().getSpawn().isSendPickaxe() || game.getArena().getRespawn().isSendPickaxe()) {
 
             ItemStack pickaxe = new ItemStack(Material.IRON_PICKAXE);

+ 64 - 11
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/misc/MissileWarsPlaceholder.java

@@ -19,7 +19,7 @@
 package de.butzlabben.missilewars.game.misc;
 
 import de.butzlabben.missilewars.MissileWars;
-import de.butzlabben.missilewars.configuration.Lobby;
+import de.butzlabben.missilewars.configuration.lobby.Lobby;
 import de.butzlabben.missilewars.configuration.arena.Arena;
 import de.butzlabben.missilewars.game.Game;
 import de.butzlabben.missilewars.game.GameManager;
@@ -32,6 +32,7 @@ import org.jetbrains.annotations.NotNull;
 public class MissileWarsPlaceholder extends PlaceholderExpansion {
 
     private final MissileWars plugin;
+    private final String noInformation = "&7?";
 
     public MissileWarsPlaceholder(MissileWars plugin) {
         this.plugin = plugin;
@@ -52,7 +53,7 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
     @Override
     @NotNull
     public String getVersion() {
-        return "0.0.1";
+        return "0.0.2";
     }
 
     // This is required or else PlaceholderAPI will unregister the expansion on reload
@@ -66,7 +67,7 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
 
         if (params.endsWith("_this") || params.startsWith("player_")) {
             // if (!offlinePlayer.isOnline()) return "§c§oPlayer is not online!";
-            if (!offlinePlayer.isOnline()) return "";
+            if (!offlinePlayer.isOnline()) return noInformation;
 
             Player player = offlinePlayer.getPlayer();
             Game playerGame = GameManager.getInstance().getGame(player.getLocation());
@@ -79,11 +80,16 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
                 }
 
                 // if (params.startsWith("lobby_")) return "§c§oThis is not a lobby area!";
-                if (params.startsWith("lobby_")) return "";
+                if (params.startsWith("lobby_")) return noInformation;
                 // if (params.startsWith("arena_")) return "§c§oThis is not a game arena!";
-                if (params.startsWith("arena_")) return "";
+                if (params.startsWith("arena_")) return noInformation;
                 // if (params.startsWith("player_")) return "§c§oPlayer is not in a game!";
-                if (params.startsWith("player_")) return "";
+                if (params.startsWith("player_")) return noInformation;
+            }
+            
+            if (playerGame.getArena() == null) {
+                // if (params.startsWith("arena_")) return "§c§oThis is not a game arena!";
+                if (params.startsWith("arena_")) return noInformation;
             }
 
             if (params.startsWith("lobby_")) params = params.replace("this", playerGame.getLobby().getName());
@@ -98,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())) {
@@ -106,22 +117,59 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
             
             // %missilewars_lobby_team1_name_<lobby name or 'this'>%
             if (params.equalsIgnoreCase("lobby_team1_name_" + lobby.getName())) {
-                return lobby.getTeam1Name();
+                return lobby.getTeam1Config().getName();
             }
             
             // %missilewars_lobby_team1_color_<lobby name or 'this'>%
             if (params.equalsIgnoreCase("lobby_team1_color_" + lobby.getName())) {
-                return lobby.getTeam1Color();
+                return lobby.getTeam1Config().getColor();
             }
             
             // %missilewars_lobby_team2_name_<lobby name or 'this'>%
             if (params.equalsIgnoreCase("lobby_team2_name_" + lobby.getName())) {
-                return lobby.getTeam2Name();
+                return lobby.getTeam2Config().getName();
             }
             
             // %missilewars_lobby_team2_color_<lobby name or 'this'>%
             if (params.equalsIgnoreCase("lobby_team2_color_" + lobby.getName())) {
-                return lobby.getTeam2Color();
+                return lobby.getTeam2Config().getColor();
+            }
+            
+            // %missilewars_lobby_mapchooseprocedure_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_mapchooseprocedure_" + lobby.getName())) {
+                return lobby.getMapChooseProcedure().toString();
+            }
+            
+            // %missilewars_lobby_gameduration_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_gameduration_" + lobby.getName())) {
+                return Integer.toString(game.getGameDuration());
+            }
+            
+            // %missilewars_lobby_arenasize_X_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_arenasize_X_" + lobby.getName())) {
+                if (game.getGameArea() != null) {
+                    return Integer.toString(game.getGameArea().getXSize());
+                } else {
+                    return noInformation;
+                }
+            }
+            
+            // %missilewars_lobby_arenasize_Y_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_arenasize_Y_" + lobby.getName())) {
+                if (game.getGameArea() != null) {
+                    return Integer.toString(game.getGameArea().getYSize());
+                } else {
+                    return noInformation;
+                }
+            }
+            
+            // %missilewars_lobby_arenasize_Z_<lobby name or 'this'>%
+            if (params.equalsIgnoreCase("lobby_arenasize_Z_" + lobby.getName())) {
+                if (game.getGameArea() != null) {
+                    return Integer.toString(game.getGameArea().getZSize());
+                } else {
+                    return noInformation;
+                }
             }
             
             for (Arena arena : lobby.getArenas()) {
@@ -135,7 +183,12 @@ public class MissileWarsPlaceholder extends PlaceholderExpansion {
                 if (params.equalsIgnoreCase("arena_missileamount_" + arena.getName())) {
                     return Integer.toString(arena.getMissileConfiguration().getSchematics().size());
                 }
-
+                
+                // %missilewars_arena_gameduration_<arena name or 'this'>%
+                if (params.equalsIgnoreCase("arena_gameduration_" + arena.getName())) {
+                    return Integer.toString(arena.getGameDuration());
+                }
+                
             }
             
             if (game.getPlayers().get(offlinePlayer.getUniqueId()) != null) {

+ 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());
         }
     }

+ 6 - 25
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
@@ -88,7 +84,7 @@ public class ScoreboardManager {
         }
         obj = board.registerNewObjective("Info", "dummy");
         obj.setDisplaySlot(DisplaySlot.SIDEBAR);
-        obj.setDisplayName(ChatColor.translateAlternateColorCodes('&', SCOREBOARD_TITLE));
+        obj.setDisplayName(SCOREBOARD_TITLE);
 
         // check if the team lists are used
         for (String cleanLine : SCOREBOARD_ENTRIES) {
@@ -243,22 +239,7 @@ 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 = ChatColor.translateAlternateColorCodes('&', text);
-
+        
         text = text.replace("%team1%", team1.getFullname());
         text = text.replace("%team2%", team2.getFullname());
 
@@ -271,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);

+ 7 - 11
missilewars-plugin/src/main/java/de/butzlabben/missilewars/game/timer/LobbyTimer.java

@@ -48,18 +48,15 @@ public class LobbyTimer extends Timer implements Runnable {
             if (mwPlayer.getPlayer() == null) continue;
             mwPlayer.getPlayer().setLevel(seconds);
         }
-
-        int size1 = getGame().getTeam1().getMembers().size();
-        int size2 = getGame().getTeam2().getMembers().size();
-
-        if (size1 == 0 || size2 == 0) {
+        
+        if (getGame().getTeamManager().hasEmptyPlayerTeam()) {
             seconds = startTime;
             return;
         }
-
+        
         --remaining;
         if (remaining == 0) {
-            if (size1 + size2 < getGame().getLobby().getMinSize()) {
+            if (getGame().areToFewPlayers()) {
                 seconds = startTime;
                 remaining = 90;
                 broadcast(Messages.getMessage(true, Messages.MessageEnum.LOBBY_NOT_ENOUGH_PLAYERS));
@@ -86,8 +83,7 @@ public class LobbyTimer extends Timer implements Runnable {
                 playPling();
                 break;
             case 0:
-                int diff = size1 - size2;
-                if (diff >= 2 || diff <= -2) {
+                if (!getGame().getTeamManager().hasBalancedTeamSizes()) {
                     broadcast(Messages.getMessage(true, Messages.MessageEnum.LOBBY_TEAMS_UNEQUAL));
                     seconds = startTime;
                     return;
@@ -112,9 +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();
     }
-
+    
 }

+ 0 - 77
missilewars-plugin/src/main/java/de/butzlabben/missilewars/inventory/VoteInventory.java

@@ -1,77 +0,0 @@
-/*
- * 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.inventory;
-
-import de.butzlabben.missilewars.Logger;
-import de.butzlabben.missilewars.configuration.Messages;
-import de.butzlabben.missilewars.configuration.arena.Arena;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import org.bukkit.Material;
-
-public class VoteInventory extends OrcInventory {
-
-    public VoteInventory(List<Arena> arenas) {
-        super(Messages.getMessage(false, Messages.MessageEnum.VOTE_GUI), (int) Math.ceil(arenas.size() / 9D));
-
-        Map<Integer, int[]> map = new HashMap<>();
-        map.put(1, new int[] {4});
-        map.put(2, new int[] {0, 8});
-        map.put(3, new int[] {0, 4, 8});
-        map.put(4, new int[] {0, 3, 5, 8});
-        map.put(5, new int[] {0, 2, 4, 6, 8});
-        map.put(6, new int[] {0, 1, 3, 5, 7, 8});
-        map.put(7, new int[] {0, 1, 3, 4, 5, 7, 8});
-        map.put(8, new int[] {0, 1, 2, 3, 5, 6, 7, 8});
-        map.put(0, new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8});
-
-        final int rowCount = (int) Math.ceil(arenas.size() / 9D);
-        int currentRow = 0;
-        int inRowIndex = 0;
-        for (Arena arena : arenas) {
-            Material material;
-            try {
-                material = Material.valueOf(arena.getDisplayMaterial());
-            } catch (IllegalArgumentException ignored) {
-                Logger.WARN.log("Could not find a material with the name: " + arena.getDisplayMaterial());
-                material = Material.BARRIER;
-            }
-            OrcItem orcItem = new OrcItem(material, arena.getDisplayName());
-            orcItem.setOnClick((p, inv, item) -> {
-                p.performCommand("mw vote " + arena.getName());
-                p.closeInventory();
-            });
-            int index;
-            if (currentRow >= rowCount - 1) {
-                index = map.get(arenas.size() % 9)[inRowIndex];
-            } else {
-                index = map.get(0)[inRowIndex];
-            }
-            index += currentRow * 9;
-            addItem(index, orcItem);
-            inRowIndex++;
-            if (inRowIndex == 9) {
-                inRowIndex = 0;
-                currentRow++;
-            }
-        }
-    }
-}
-

+ 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);
     }
 }

+ 1 - 1
missilewars-plugin/src/main/java/de/butzlabben/missilewars/listener/game/GameBoundListener.java

@@ -69,6 +69,6 @@ public abstract class GameBoundListener implements Listener {
         if (mwPlayer == null) return;
 
         mwPlayer.setPlayerInteractEventCancel(true);
-        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> mwPlayer.setPlayerInteractEventCancel(false), 20);
+        Bukkit.getScheduler().runTaskLater(MissileWars.getInstance(), () -> mwPlayer.setPlayerInteractEventCancel(false), 10);
     }
 }

+ 73 - 14
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);
@@ -186,7 +191,7 @@ public class GameListener extends GameBoundListener {
             event.setRespawnLocation(team.getSpawn());
             getGame().getEquipmentManager().sendGameItems(player, true);
             getGame().setPlayerAttributes(player);
-            getGame().getPlayer(player).getRandomGameEquipment().resetPlayerInterval();
+            getGame().getPlayer(player).getPlayerEquipmentRandomizer().resetPlayerInterval();
 
             FallProtectionConfiguration fallProtection = getGame().getArena().getFallProtection();
             if (fallProtection.isEnabled()) {
@@ -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;
+            case "has permission":
+                return Type.HAS_PERMISSION;
+                
+            case "!string equals":
+                negateRequirement = true;
+            case "string equals":
+                return Type.STRING_EQUALS;
+                
+            case "!string equals ignorecase":
+                negateRequirement = true;
+            case "string equals ignorecase":
+                return Type.STRING_EQUALS_IGNORE_CASE;
+                
+            case "!string contains":
+                negateRequirement = true;
+            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-item}")) {
+            tempItem = mwPlayer.getTeam().getMenuItem();
+            
+        } else {
+            tempItem = new ItemStack(Material.valueOf(materialName.toUpperCase()));
+            
+        }
+        
+        updatePapiValues(mwPlayer.getPlayer());
+        
+        // initial ItemMeta values:
+        
+        ItemMeta itemMeta = tempItem.getItemMeta();
+        itemMeta.setLore(finalLoreList);
+        tempItem.setItemMeta(itemMeta);
+        MenuItem.hideMetaValues(tempItem);
+        MenuItem.setDisplayName(tempItem, finalDisplayName);
+        
+        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));
+        }
+    }
+    
+}

+ 35 - 4
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;
@@ -44,27 +48,54 @@ public class MWPlayer implements Runnable {
     private final Game game;
     @Setter
     private Team team;
-    @Setter
-    private PlayerEquipmentRandomizer randomGameEquipment;
+    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() {
         return Bukkit.getPlayer(uuid);
     }
-
+    
+    public void iniPlayerEquipmentRandomizer() {
+        this.playerEquipmentRandomizer = new PlayerEquipmentRandomizer(this, game);
+    }
+    
     @Override
     public void run() {
-        randomGameEquipment.tick();
+        playerEquipmentRandomizer.tick();
     }
 
     @Override
     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));
+    }
 }

+ 6 - 1
pom.xml

@@ -131,13 +131,18 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
-                <version>3.2.1</version>
+                <version>3.5.2</version>
                 <configuration>
+                    <dependencyReducedPomLocation>${project.build.directory}/dependency-reduced-pom.xml</dependencyReducedPomLocation>
                     <relocations>
                         <relocation>
                             <pattern>org.bstats</pattern>
                             <shadedPattern>de.butzlabben.missilewars</shadedPattern>
                         </relocation>
+                        <relocation>
+                            <pattern>com.github.stefvanschie.inventoryframework</pattern>
+                            <shadedPattern>de.butzlabben.missilewars.MissileWars.inventoryframework</shadedPattern>
+                        </relocation>
                     </relocations>
 
                     <filters>