diff options
11 files changed, 187 insertions, 62 deletions
| diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java index 2d39c83..bf130ea 100644 --- a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java +++ b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStart.java @@ -32,6 +32,7 @@ public class ForceStart implements SubCommand, CommandExecutor, TabCompleter {          Game game;          if (args.length < 1 || args[0] == null) { +            // no arena passed in, try to infer from game player is in              game = arenaManager.findGamePlayerIsIn((Player)sender);              if (game == null) {                  sender.sendMessage(languageManager.fromKey("missing-arena-parameter")); diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java index 24f430c..8ab48c2 100644 --- a/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java +++ b/src/main/java/com/MylesAndMore/Tumble/commands/ForceStop.java @@ -32,6 +32,7 @@ public class ForceStop implements SubCommand, CommandExecutor, TabCompleter {          Game game;          if (args.length < 1 || args[0] == null) { +            // no arena passed in, try to infer from game player is in              game = arenaManager.findGamePlayerIsIn((Player)sender);              if (game == null) {                  sender.sendMessage(languageManager.fromKey("missing-arena-parameter")); diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Join.java b/src/main/java/com/MylesAndMore/Tumble/commands/Join.java index 1dbbad6..3177f3e 100644 --- a/src/main/java/com/MylesAndMore/Tumble/commands/Join.java +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Join.java @@ -59,6 +59,7 @@ public class Join implements SubCommand, CommandExecutor, TabCompleter {          Game game;          if (args.length < 2 || args[1] == null) { +            // try to infer game type from game taking place in the arena              if (arena.game == null) {                  sender.sendMessage(languageManager.fromKey("specify-game-type"));                  return false; @@ -80,10 +81,12 @@ public class Join implements SubCommand, CommandExecutor, TabCompleter {              }              if (arena.game == null) { +                // no game is taking place in this arena, start one                  game = arena.game = new Game(arena, type);              }              else              { +                // a game is taking place in this arena, check that it is the right type                  if (arena.game.type == type) {                      game = arena.game;                  } @@ -96,6 +99,7 @@ public class Join implements SubCommand, CommandExecutor, TabCompleter {              }          } +        // check to make sure the arena has a game spawn          if (game.arena.gameSpawn == null) {              if (p.isOp()) {                  sender.sendMessage(languageManager.fromKey("arena-not-ready-op")); diff --git a/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java b/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java index 2cf5b90..67213ee 100644 --- a/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java +++ b/src/main/java/com/MylesAndMore/Tumble/commands/Tumble.java @@ -42,6 +42,7 @@ public class Tumble implements CommandExecutor, TabCompleter {              return false;          } +        // pass command action through to subCommand          subCmd.onCommand(sender, command, args[0], removeFirst(args));          return true;      } @@ -49,6 +50,7 @@ public class Tumble implements CommandExecutor, TabCompleter {      @Override      public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {          if (args.length == 1) { +            // show only subCommands the user has permission for              ArrayList<String> PermittedSubCmds = new ArrayList<>();              for (SubCommand subCmd: subCommands.values()) {                  if (sender.hasPermission(subCmd.getPermission())) { @@ -63,6 +65,7 @@ public class Tumble implements CommandExecutor, TabCompleter {                  return Collections.emptyList();              } +            // pass tab complete through to subCommand              if (subCommands.get(args[0]) instanceof TabCompleter tcmp) {                  return tcmp.onTabComplete(sender, command, args[0], removeFirst(args));              } @@ -74,13 +77,23 @@ public class Tumble implements CommandExecutor, TabCompleter {          return Collections.emptyList();      } +    /** +     * Create a copy of an array with the first element removed +     * @param arr the source array +     * @return the source without the first element +     */      private String[] removeFirst(String[] arr) {          ArrayList<String> tmp = new ArrayList<>(List.of(arr));          tmp.remove(0);          return tmp.toArray(new String[0]);      } -    private static Map.Entry<String, SubCommand> CmdNameAsKey(SubCommand s) { -        return Map.entry(s.getCommandName(),s); +    /** +     * Creates a map entry with the name of the subCommand as the key and the subCommand itself as the value +     * @param cmd The subCommand to use +     * @return A map entry from the subCommand +     */ +    private static Map.Entry<String, SubCommand> CmdNameAsKey(SubCommand cmd) { +        return Map.entry(cmd.getCommandName(),cmd);      }  } diff --git a/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java b/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java index aa29cf1..b4364d3 100644 --- a/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java +++ b/src/main/java/com/MylesAndMore/Tumble/config/ArenaManager.java @@ -18,6 +18,9 @@ import java.util.Objects;  import static com.MylesAndMore.Tumble.Main.plugin; +/** + * Manages arenas.yml and stores list of arenas + */  public class ArenaManager {      public HashMap<String, Arena> arenas; @@ -25,11 +28,17 @@ public class ArenaManager {      private final CustomConfig arenasYml = new CustomConfig("arenas.yml");      private final FileConfiguration config = arenasYml.getConfig(); +    /** +     * Create an ArenaManager +     */      public ArenaManager() {          arenasYml.saveDefaultConfig();          readConfig();      } +    /** +     * Read arenas from arenas.ynl and populate this.arenas +     */      public void readConfig() {          // arenas @@ -78,6 +87,9 @@ public class ArenaManager {          }      } +    /** +     * Write arenas from this.arenas to arenas.yml +     */      public void WriteConfig() {          config.set("arenas", null); // clear everything @@ -118,16 +130,16 @@ public class ArenaManager {      }      /** -     * tries to convert a config section in the following format to a world +     * Tries to convert a config section in the following format to a world       * section:       *   x:       *   y:       *   z:       *   world: -     * @param section the section in the yaml with x, y, z, and world as its children -     * @return result of either: -     *   success = true and a world -     *   success = false and an error string +     * @param section The section in the yaml with x, y, z, and world as its children +     * @return Result of either: +     *   Result#success = true and Result#value OR +     *   Result#success = false and Result#error       */      private Result<Location> readWorld(@Nullable ConfigurationSection section) { @@ -155,6 +167,16 @@ public class ArenaManager {          return new Result<>(new Location(world,x,y,z));      } +    /** +     * Write a location into the config using the following format: +     * section: +     *   x: +     *   y: +     *   z: +     *   world: +     * @param path The path of the section to write +     * @param location The location to write +     */      private void WriteWorld(String path, @NotNull Location location) {          ConfigurationSection section = config.getConfigurationSection(path); diff --git a/src/main/java/com/MylesAndMore/Tumble/config/ConfigManager.java b/src/main/java/com/MylesAndMore/Tumble/config/ConfigManager.java index 3b56f7f..13cc779 100644 --- a/src/main/java/com/MylesAndMore/Tumble/config/ConfigManager.java +++ b/src/main/java/com/MylesAndMore/Tumble/config/ConfigManager.java @@ -7,6 +7,9 @@ import java.util.Objects;  import static com.MylesAndMore.Tumble.Main.plugin; +/** + * Manages config.yml and stores its options + */  public class ConfigManager {      public boolean HideLeaveJoin;      public int waitDuration; @@ -15,12 +18,18 @@ public class ConfigManager {      private final Configuration config = configYml.getConfig();      private final Configuration defaultConfig = Objects.requireNonNull(config.getDefaults()); +    /** +     * Create a config manager +     */      public ConfigManager() {          configYml.saveDefaultConfig();          validate();          readConfig();      } +    /** +     * Check keys of config.yml against the defaults +     */      public void validate() {          boolean invalid = false;          for (String key : defaultConfig.getKeys(true)) { @@ -34,6 +43,9 @@ public class ConfigManager {          }      } +    /** +     * Reads options in from config.yml +     */      public void readConfig() {          HideLeaveJoin = config.getBoolean("hide-join-leave-messages", false);          waitDuration = config.getInt("wait-duration", 15); diff --git a/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java b/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java index 917ce78..7c82664 100644 --- a/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java +++ b/src/main/java/com/MylesAndMore/Tumble/config/LanguageManager.java @@ -8,16 +8,25 @@ import java.util.Objects;  import static com.MylesAndMore.Tumble.Main.plugin; +/** + * Manages language.yml and allows retrieval of keys + */  public class LanguageManager {      private final CustomConfig languageYml = new CustomConfig("language.yml");      private final Configuration config = languageYml.getConfig();      private final Configuration defaultConfig = Objects.requireNonNull(config.getDefaults()); +    /** +     * Creates a new LanguageManager +     */      public LanguageManager() {          languageYml.saveDefaultConfig();          validate();      } +    /** +     * Check keys of language.yml against the defaults +     */      public void validate() {          boolean invalid = false;          for (String key : defaultConfig.getKeys(true)) { @@ -31,10 +40,22 @@ public class LanguageManager {          }      } +    /** +     * Gets a key from language.yml and prepends the prefix. +     * If it is not present, a default value will be returned +     * @param key The key representing the message +     * @return The message from the key +     */      public String fromKey(String key) {          return fromKeyNoPrefix("prefix") + fromKeyNoPrefix(key);      } +    /** +     * Gets a key from language.yml. +     * If it is not present, a default value will be returned +     * @param key The key representing the message +     * @return The message from the key +     */      public String fromKeyNoPrefix(String key) {          String val = config.getString(key); diff --git a/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java b/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java index 7abf774..1bc31b3 100644 --- a/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java +++ b/src/main/java/com/MylesAndMore/Tumble/game/EventListener.java @@ -24,12 +24,16 @@ import static com.MylesAndMore.Tumble.Main.configManager;  import static com.MylesAndMore.Tumble.Main.plugin;  /** - * Tumble event listener for all plugin and game-related events. + * An event listener for a game of tumble.   */  public class EventListener implements Listener { -      World gameWorld;      Game game; + +    /** +     * Create a new EventListener +     * @param game The game that the EventListener belongs to. +     */      public EventListener(Game game) {          this.game = game;          this.gameWorld = game.arena.gameSpawn.getWorld(); diff --git a/src/main/java/com/MylesAndMore/Tumble/game/Game.java b/src/main/java/com/MylesAndMore/Tumble/game/Game.java index 20d8869..169ce33 100644 --- a/src/main/java/com/MylesAndMore/Tumble/game/Game.java +++ b/src/main/java/com/MylesAndMore/Tumble/game/Game.java @@ -35,15 +35,41 @@ public class Game {      private List<Player> playersAlive;      private EventListener eventListener; +    /** +     * Create a new Game +     * @param arena The arena the game is taking place in +     * @param type The game type +     */      public Game(@NotNull Arena arena, @NotNull GameType type) {          this.arena = arena;          this.type = type;          this.gameSpawn = arena.gameSpawn; +    } + +    /** +     * Adds a player to the wait area. Called from /tmbl join +     * Precondition: the game is in state WAITING +     * @param p Player to add +     */ +    public void addPlayer(Player p) { +        gamePlayers.add(p); +        if (arena.waitArea != null) { +            inventories.put(p,p.getInventory().getContents()); +            p.teleport(arena.waitArea); +            p.getInventory().clear(); +        } +        if (gamePlayers.size() >= 2 && gameState == GameState.WAITING) { +            autoStart(); +        } +        else { +            displayActionbar(Collections.singletonList(p), languageManager.fromKeyNoPrefix("waiting-for-players")); +        }      }      /** -     * Creates a new Game +     * Starts the game +     * Called from /tmbl forceStart or after the wait counter finishes       */      public void gameStart() { @@ -52,12 +78,15 @@ public class Game {              return;          } +        // cancel wait timer          Bukkit.getServer().getScheduler().cancelTask(autoStartID);          autoStartID = -1; +        // register event listener          eventListener = new EventListener(this);          Bukkit.getServer().getPluginManager().registerEvents(eventListener, plugin); +        // save inventories (if not already done)          for (Player p : gamePlayers) {              if (!inventories.containsKey(p)) {                  inventories.put(p, p.getInventory().getContents()); @@ -68,20 +97,26 @@ public class Game {      }      /** -     * Starts a new round +     * Starts a round       */      private void roundStart() {          gameState = GameState.STARTING;          playersAlive = new ArrayList<>(gamePlayers); +          scatterPlayers(gamePlayers);          // Put all players in spectator to prevent them from getting kicked for flying          setGamemode(gamePlayers, GameMode.SPECTATOR); -        Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> setGamemode(gamePlayers, GameMode.SPECTATOR), 10); +        // do it again in case they were not in the world yet +        Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { +            setGamemode(gamePlayers, GameMode.SPECTATOR); +        }, 10); +          clearInventories(gamePlayers);          clearArena();          prepareGameType(type); + +        // Begin the countdown sequence          Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { -            // Begin the countdown sequence              countdown(() -> {                  setGamemode(gamePlayers, GameMode.SURVIVAL);                  gameState = GameState.RUNNING; @@ -91,42 +126,45 @@ public class Game {      /**       * Type specific setup: Generating layers and giving items -     * @param type can be either "shovels", "snowballs", or "mixed" +     * @param type game type,       */      private void prepareGameType(GameType type) { -        roundType = type; // note: may need deepcopy this for it to work properly -        if (roundType.equals(GameType.MIXED)) { -            // Randomly select either shovels or snowballs and re-run the method -            Random random = new Random(); -            switch (random.nextInt(2)) { -                case 0 -> roundType = GameType.SHOVELS; -                case 1 -> roundType = GameType.SNOWBALLS; -            } -        } - -        switch (roundType) { +        roundType = type; +        switch (type) {              case SHOVELS -> {                  Generator.generateLayersShovels(gameSpawn.clone()); +                  ItemStack shovel = new ItemStack(Material.IRON_SHOVEL);                  shovel.addEnchantment(Enchantment.SILK_TOUCH, 1);                  giveItems(gamePlayers, shovel); -                // Schedule a process to give snowballs after 2m30s (so people can't island, the OG game had this); add 160t because of the countdown + +                // Schedule a process to give snowballs after 2m30s (so people can't island, the OG game had this); +                // add 160t because of the countdown                  gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> {                      clearInventories(gamePlayers);                      giveItems(gamePlayers, new ItemStack(Material.SNOWBALL));                      displayActionbar(gamePlayers, languageManager.fromKeyNoPrefix("showdown"));                      playSound(gamePlayers, Sound.ENTITY_ELDER_GUARDIAN_CURSE, SoundCategory.HOSTILE, 1, 1); +                      // End the round in another 2m30s                      gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundEnd, 3000);                  }, 3160);              }              case SNOWBALLS -> {                  Generator.generateLayersSnowballs(gameSpawn.clone()); +                  giveItems(gamePlayers, new ItemStack(Material.SNOWBALL));                  // End the round in 5m                  gameID = Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundEnd, 6160);              } +            case MIXED -> { +                Random random = new Random(); +                switch (random.nextInt(2)) { +                    case 0 -> prepareGameType(GameType.SHOVELS); +                    case 1 -> prepareGameType(GameType.SNOWBALLS); +                } +            }          }      } @@ -138,8 +176,9 @@ public class Game {          gameState = GameState.ENDING;          Bukkit.getServer().getScheduler().cancelTask(gameID);          gameID = -1; -        // Clear old layers (as a fill command, this would be /fill ~-20 ~-20 ~-20 ~20 ~ ~20 relative to spawn) +          playSound(gamePlayers, Sound.BLOCK_NOTE_BLOCK_PLING, SoundCategory.BLOCKS, 5, 0); +          // Check if there was a definite winner or not          if (!playersAlive.isEmpty()) {              Player winner = playersAlive.get(0); @@ -148,11 +187,11 @@ public class Game {                  gameWins.put(winner, 0);              }              gameWins.put(winner, gameWins.get(winner)+1); +              if (gameWins.get(winner) == 3) {                  gameEnd();              } -            // If that player doesn't have three wins, nobody else does, so we need another round -            else { +            else { // If that player doesn't have three wins, nobody else does, so we need another round                  displayTitles(gamePlayers, languageManager.fromKeyNoPrefix("round-over"), languageManager.fromKeyNoPrefix("round-winner").replace("%winner%", winner.getDisplayName()), 5, 60, 5);                  Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, this::roundStart, 100);              } @@ -173,33 +212,35 @@ public class Game {              setGamemode(gamePlayers, GameMode.SPECTATOR);              clearInventories(gamePlayers); +            // display winner              Player winner = getPlayerWithMostWins(gameWins);              if (winner != null) {                  displayTitles(gamePlayers, languageManager.fromKeyNoPrefix("game-over"), languageManager.fromKeyNoPrefix("game-winner").replace("%winner%",winner.getDisplayName()), 5, 60, 5);              } +              displayActionbar(gamePlayers, languageManager.fromKeyNoPrefix("lobby-in-10"));              // Wait 10s (200t), then              Bukkit.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { -                  clearArena(); +                // teleport player back and restore inventory                  for (Player p : gamePlayers) { -                    // Restore inventories -                    if (inventories.containsKey(p)) { -                        p.getInventory().setContents(inventories.get(p)); -                    } -                      if (p == winner && arena.winnerLobby != null) {                          p.teleport(arena.winnerLobby);                      }                      else {                          p.teleport(Objects.requireNonNull(arena.lobby));                      } + +                    if (inventories.containsKey(p)) { +                        p.getInventory().setContents(inventories.get(p)); +                    }                  }              }, 200);          } +          Bukkit.getServer().getScheduler().cancelTask(gameID);          gameID = -1;          Bukkit.getServer().getScheduler().cancelTask(autoStartID); @@ -208,8 +249,13 @@ public class Game {          arena.game = null;      } +    /** +     * Stops the game, usually while it is still going +     * called if too many players leave, or from /tmbl forceStop +     */      public void stopGame() {          gamePlayers.forEach(this::removePlayer); +          Bukkit.getServer().getScheduler().cancelTask(gameID);          gameID = -1;          Bukkit.getServer().getScheduler().cancelTask(autoStartID); @@ -224,48 +270,37 @@ public class Game {       * @param p Player to remove       */      public void removePlayer(Player p) { +        gamePlayers.remove(p); +        // check if the game has not started yet          if (gameState == GameState.WAITING) { -            gamePlayers.remove(p); + +            // inform player that there are no longer enough players to start              if (gamePlayers.size() < 2) {                  displayActionbar(gamePlayers, languageManager.fromKeyNoPrefix("waiting-for-players"));              } +            // teleport player back and restore inventory              if (arena.waitArea != null) { +                p.getInventory().clear();                  p.teleport(arena.lobby); +                if (inventories.containsKey(p)) { +                    p.getInventory().setContents(inventories.get(p)); +                }              }          }          else { -            gamePlayers.remove(p); +            // stop the game if there are not enough players              if (gamePlayers.size() < 2) { -                gameEnd(); +                stopGame();              } + +            // teleport player back and restore inventory              p.getInventory().clear(); +            p.teleport(arena.lobby);              if (inventories.containsKey(p)) {                  p.getInventory().setContents(inventories.get(p));              } -            p.teleport(arena.lobby); -        } -    } - -    /** -     * Adds a player to the wait area. Called from /tumble-join -     * Precondition: the game is in state WAITING -     * @param p Player to add -     */ -    public void addPlayer(Player p) { -        gamePlayers.add(p); -        // save inventory -        if (arena.waitArea != null) { -            inventories.put(p,p.getInventory().getContents()); -            p.teleport(arena.waitArea); -            p.getInventory().clear(); -        } -        if (gamePlayers.size() >= 2 && gameState == GameState.WAITING) { -            autoStart(); -        } -        else { -            displayActionbar(Collections.singletonList(p), languageManager.fromKeyNoPrefix("waiting-for-players"));          }      } diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java b/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java index b77a59d..8ef4638 100644 --- a/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/CustomConfig.java @@ -12,11 +12,19 @@ import java.util.logging.Level;  import static com.MylesAndMore.Tumble.Main.plugin; +/** + * Allows additional configs to be created with the same saving methods as the default config + * Most code is copied from {@link org.bukkit.plugin.java.JavaPlugin} + */  public class CustomConfig {      private FileConfiguration newConfig = null;      private final File configFile;      private final String fileName; +    /** +     * Create a new CustomConfig +     * @param fileName Name of the YAML file to create +     */      public CustomConfig(String fileName) {          this.fileName = fileName;          this.configFile = new File(plugin.getDataFolder(), fileName); diff --git a/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java b/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java index 2158584..cc09527 100644 --- a/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java +++ b/src/main/java/com/MylesAndMore/Tumble/plugin/SubCommand.java @@ -2,7 +2,11 @@ package com.MylesAndMore.Tumble.plugin;  import org.bukkit.command.CommandExecutor; +/** + * Requires that subCommands have a commandName and permission getter. + * This allows the permission and commandName to be checked from the base command. + */  public interface SubCommand extends CommandExecutor { -    public String getCommandName(); -    public String getPermission(); +    String getCommandName(); +    String getPermission();  } | 
