duplicates = new ArrayList<>();
+
+ public StandaloneArena(String name, Location location1, Location location2) {
+ super(name, ArenaType.STANDALONE, location1, location2);
+ }
+
+ @Override
+ public void save() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getArenaConfig(), "arenas." + this.name);
+
+ cursor.set(null);
+ cursor.set("type", ArenaType.STANDALONE.name());
+ cursor.set("spawn1", LocationUtil.serialize(this.spawn1));
+ cursor.set("spawn2", LocationUtil.serialize(this.spawn2));
+ cursor.set("cuboid.location1", LocationUtil.serialize(this.getLowerCorner()));
+ cursor.set("cuboid.location2", LocationUtil.serialize(this.getUpperCorner()));
+ cursor.set("ladders", this.getLadders());
+
+ if (!this.duplicates.isEmpty()) {
+ AtomicInteger i = new AtomicInteger();
+
+ this.duplicates.forEach(duplicate -> {
+ cursor.setPath("arenas." + this.name + ".duplicates." + i.intValue());
+ cursor.set("cuboid.location1", LocationUtil.serialize(duplicate.getLowerCorner()));
+ cursor.set("cuboid.location2", LocationUtil.serialize(duplicate.getUpperCorner()));
+ cursor.set("spawn1", LocationUtil.serialize(duplicate.getSpawn1()));
+ cursor.set("spawn2", LocationUtil.serialize(duplicate.getSpawn2()));
+
+ i.getAndIncrement();
+ });
+ }
+
+ cursor.save();
+ }
+
+ @Override
+ public void delete() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getArenaConfig(), "arenas." + this.name);
+
+ cursor.set(null);
+ cursor.save();
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/arena/generator/ArenaGenerator.java b/plugin/src/main/java/me/joeleoli/praxi/arena/generator/ArenaGenerator.java
new file mode 100644
index 0000000..880d01c
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/arena/generator/ArenaGenerator.java
@@ -0,0 +1,176 @@
+package me.joeleoli.praxi.arena.generator;
+
+import com.boydti.fawe.util.TaskManager;
+import java.io.File;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.Nucleus;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.arena.ArenaType;
+import me.joeleoli.praxi.arena.SharedArena;
+import me.joeleoli.praxi.arena.StandaloneArena;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.Sign;
+
+@AllArgsConstructor
+public class ArenaGenerator {
+
+ private String name;
+ private World world;
+ private Schematic schematic;
+ private ArenaType type;
+
+ public void generate(File file, StandaloneArena parentArena) {
+ if (Arena.getByName(this.name) != null) {
+ this.name = name + Nucleus.RANDOM.nextInt(1000);
+ }
+
+ this.log("Generating " + this.type.name() + " " + this.name + " arena...");
+
+ int range = 500;
+ int attempts = 0;
+
+ int preciseX = Nucleus.RANDOM.nextInt(range);
+ int preciseZ = Nucleus.RANDOM.nextInt(range);
+
+ if (Nucleus.RANDOM.nextBoolean()) {
+ preciseX = -preciseX;
+ }
+
+ if (Nucleus.RANDOM.nextBoolean()) {
+ preciseZ = -preciseZ;
+ }
+
+ top:
+ while (true) {
+ attempts++;
+
+ if (attempts >= 5) {
+ preciseX = Nucleus.RANDOM.nextInt(range);
+ preciseZ = Nucleus.RANDOM.nextInt(range);
+
+ if (Nucleus.RANDOM.nextBoolean()) {
+ preciseX = -preciseX;
+ }
+
+ if (Nucleus.RANDOM.nextBoolean()) {
+ preciseZ = -preciseZ;
+ }
+
+ range += 500;
+
+ this.log("Increased range to: " + range);
+ }
+
+ if (this.world.getBlockAt(preciseX, 72, preciseZ) == null) {
+ continue;
+ }
+
+ final int minX = preciseX - this.schematic.getClipBoard().getWidth() - 200;
+ final int maxX = preciseX + this.schematic.getClipBoard().getWidth() + 200;
+ final int minZ = preciseZ - this.schematic.getClipBoard().getLength() - 200;
+ final int maxZ = preciseZ + this.schematic.getClipBoard().getLength() + 200;
+ final int minY = 72;
+ final int maxY = 72 + this.schematic.getClipBoard().getHeight();
+
+ for (int x = minX; x < maxX; x++) {
+ for (int z = minZ; z < maxZ; z++) {
+ for (int y = minY; y < maxY; y++) {
+ if (this.world.getBlockAt(x, y, z).getType() != Material.AIR) {
+ continue top;
+ }
+ }
+ }
+ }
+
+ final Location minCorner = new Location(this.world, minX, minY, minZ);
+ final Location maxCorner = new Location(this.world, maxX, maxY, maxZ);
+
+ final int finalPreciseX = preciseX;
+ final int finalPreciseZ = preciseZ;
+
+ TaskManager.IMP.async(() -> {
+ try {
+ new Schematic(file).pasteSchematic(this.world, finalPreciseX, 76, finalPreciseZ);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ try {
+ Thread.sleep(1000);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ final Arena arena;
+
+ if (this.type == ArenaType.STANDALONE) {
+ arena = new StandaloneArena(this.name, minCorner, maxCorner);
+
+ this.type = ArenaType.DUPLICATE;
+
+ for (int i = 0; i < 5; i++) {
+ TaskUtil.run(() -> this.generate(file, (StandaloneArena) arena));
+ }
+ } else if (this.type == ArenaType.DUPLICATE) {
+ arena = new Arena(this.name, ArenaType.DUPLICATE, minCorner, maxCorner);
+
+ parentArena.getDuplicates().add(arena);
+ } else {
+ arena = new SharedArena(this.name, minCorner, maxCorner);
+ }
+
+ helper:
+ for (int x = minX; x < maxX; x++) {
+ for (int z = minZ; z < maxZ; z++) {
+ for (int y = minY; y < maxY; y++) {
+ if (this.world.getBlockAt(x, y, z).getType() == Material.SPONGE) {
+ final Block origin = this.world.getBlockAt(x, y, z);
+ final Block up = origin.getRelative(BlockFace.UP, 1);
+
+ if (up.getState() instanceof Sign) {
+ final Sign sign = (Sign) up.getState();
+ final float pitch = Float.valueOf(sign.getLine(0));
+ final float yaw = Float.valueOf(sign.getLine(1));
+ final Location loc =
+ new Location(origin.getWorld(), origin.getX(), origin.getY(), origin.getZ(),
+ yaw, pitch
+ );
+
+ TaskUtil.run(() -> {
+ up.setType(Material.AIR);
+ origin.setType(origin.getRelative(BlockFace.NORTH).getType());
+ });
+
+ if (arena.getSpawn1() == null) {
+ arena.setSpawn1(loc);
+ } else if (arena.getSpawn2() == null) {
+ arena.setSpawn2(loc);
+ break helper;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ arena.save();
+
+ Arena.getArenas().add(arena);
+ });
+
+ this.log(String.format("Pasted schematic at %1$s, %2$s, %3$s", preciseX, 76, preciseZ));
+
+ return;
+ }
+ }
+
+ private void log(String message) {
+ Nucleus.getInstance().getLogger().info("[ArenaGen] " + message);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/arena/generator/Schematic.java b/plugin/src/main/java/me/joeleoli/praxi/arena/generator/Schematic.java
new file mode 100644
index 0000000..ed8695c
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/arena/generator/Schematic.java
@@ -0,0 +1,42 @@
+package me.joeleoli.praxi.arena.generator;
+
+import com.sk89q.worldedit.CuboidClipboard;
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.MaxChangedBlocksException;
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldedit.bukkit.BukkitWorld;
+import com.sk89q.worldedit.schematic.SchematicFormat;
+import com.sk89q.worldedit.world.DataException;
+import java.io.File;
+import java.io.IOException;
+import lombok.Getter;
+import me.joeleoli.nucleus.Nucleus;
+import org.bukkit.World;
+
+@Getter
+public class Schematic {
+
+ private CuboidClipboard clipBoard;
+
+ public Schematic(File file) throws IOException {
+ SchematicFormat format = SchematicFormat.MCEDIT;
+
+ try {
+ this.clipBoard = format.load(file);
+ } catch (DataException e) {
+ Nucleus.getInstance().getLogger().severe("Failed to load schematic...");
+ }
+ }
+
+ public void pasteSchematic(World world, int x, int y, int z) {
+ Vector pastePos = new Vector(x, y, z);
+ EditSession editSession = new EditSession(new BukkitWorld(world), 999999);
+
+ try {
+ this.clipBoard.place(editSession, pastePos, true);
+ } catch (MaxChangedBlocksException e) {
+ e.printStackTrace();
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/arena/selection/Selection.java b/plugin/src/main/java/me/joeleoli/praxi/arena/selection/Selection.java
new file mode 100644
index 0000000..b8897a3
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/arena/selection/Selection.java
@@ -0,0 +1,91 @@
+package me.joeleoli.praxi.arena.selection;
+
+import java.util.Arrays;
+import lombok.Data;
+import lombok.NonNull;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.cuboid.Cuboid;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.metadata.FixedMetadataValue;
+
+@Data
+public class Selection {
+
+ public static final ItemStack SELECTION_WAND;
+ private static final String SELECTION_METADATA_KEY = "CLAIM_SELECTION";
+
+ static {
+ ItemStack itemStack = new ItemStack(Material.GOLD_AXE);
+ ItemMeta itemMeta = itemStack.getItemMeta();
+
+ itemMeta.setDisplayName(Style.GOLD + Style.BOLD + "Selection Wand");
+ itemMeta.setLore(Arrays.asList(
+ Style.YELLOW + "Left-click to set position 1.",
+ Style.YELLOW + "Right-click to set position 2."
+ ));
+ itemStack.setItemMeta(itemMeta);
+
+ SELECTION_WAND = itemStack;
+ }
+
+ @NonNull
+ private Location point1;
+ @NonNull
+ private Location point2;
+
+ /**
+ * Private, so that we can create a new instance in the Selection#createOrGetSelection method.
+ */
+ private Selection() {
+ }
+
+ /**
+ * Selections are stored in the player's metadata. This method removes the need to active Bukkit Metadata API calls
+ * all over the place.
+ *
+ * This method can be modified structurally as needed, the plugin only accepts Selection objects via this method.
+ *
+ * @param player the player for whom to grab the Selection object for
+ *
+ * @return selection object, either new or created
+ */
+ public static Selection createOrGetSelection(Player player) {
+ if (player.hasMetadata(SELECTION_METADATA_KEY)) {
+ return (Selection) player.getMetadata(SELECTION_METADATA_KEY).get(0).value();
+ }
+
+ Selection selection = new Selection();
+
+ player.setMetadata(SELECTION_METADATA_KEY, new FixedMetadataValue(Praxi.getInstance(), selection));
+
+ return selection;
+ }
+
+ /**
+ * @return the cuboid
+ */
+ public Cuboid getCuboid() {
+ return new Cuboid(point1, point2);
+ }
+
+ /**
+ * @return if the Selection can form a full cuboid object
+ */
+ public boolean isFullObject() {
+ return point1 != null && point2 != null;
+ }
+
+ /**
+ * Resets both locations in the Selection
+ */
+ public void clear() {
+ point1 = null;
+ point2 = null;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/board/PracticeBoardAdapter.java b/plugin/src/main/java/me/joeleoli/praxi/board/PracticeBoardAdapter.java
new file mode 100644
index 0000000..6a5e84b
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/board/PracticeBoardAdapter.java
@@ -0,0 +1,168 @@
+package me.joeleoli.praxi.board;
+
+import java.util.ArrayList;
+import java.util.List;
+import me.joeleoli.nucleus.Nucleus;
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.board.Board;
+import me.joeleoli.nucleus.board.BoardAdapter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TimeUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.player.PracticeSetting;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.queue.Queue;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.scoreboard.Scoreboard;
+
+public class PracticeBoardAdapter implements BoardAdapter {
+
+ @Override
+ public String getTitle(Player player) {
+ if (Nucleus.getInstance().getRave() != null) {
+ return Nucleus.getInstance().getRave().getRaveTask().getTitle();
+ }
+
+ return Style.PINK + Style.BOLD + "MineXD ";
+ }
+
+ @Override
+ public List getScoreboard(Player player, Board board) {
+ if (Nucleus.getInstance().getRave() != null) {
+ return Nucleus.getInstance().getRave().getRaveTask().getLines();
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (!NucleusAPI.getSetting(player, PracticeSetting.SHOW_SCOREBOARD)) {
+ return null;
+ }
+
+ final List toReturn = new ArrayList<>();
+
+ if (praxiPlayer.isInLobby()) {
+ toReturn.add(Style.YELLOW + "Online: " + Style.PINK + Bukkit.getOnlinePlayers().size());
+ toReturn.add(Style.YELLOW + "Fighting: " + Style.PINK + Praxi.getInstance().getFightingCount());
+ toReturn.add(Style.YELLOW + "Queueing: " + Style.PINK + Praxi.getInstance().getQueueingCount());
+
+ if (praxiPlayer.getParty() != null) {
+ toReturn.add(Style.YELLOW + "Your Party: " + Style.PINK + praxiPlayer.getParty().getTeamPlayers().size());
+ }
+
+ if (!Praxi.getInstance().getEventManager().getEventCooldown().hasExpired()) {
+ toReturn.add(Style.YELLOW + "Event Cooldown: " + Style.PINK + TimeUtil.millisToTimer(
+ Praxi.getInstance().getEventManager().getEventCooldown().getRemaining()));
+ }
+ } else if (praxiPlayer.isInQueue()) {
+ final Queue queue = praxiPlayer.getQueuePlayer().getQueue();
+
+ toReturn.add(Style.YELLOW + "Queue:");
+ toReturn.add(" " + Style.PINK + (queue.isRanked() ? "Ranked" : "Unranked") + " " +
+ queue.getLadder().getName());
+ toReturn.add(Style.YELLOW + "Time:");
+ toReturn.add(" " + Style.PINK + TimeUtil.millisToTimer(praxiPlayer.getQueuePlayer().getPassed()));
+
+ if (queue.isRanked()) {
+ toReturn.add(Style.YELLOW + "Range:");
+ toReturn.add(" " + Style.PINK + praxiPlayer.getQueuePlayer().getMinRange() + " -> " +
+ praxiPlayer.getQueuePlayer().getMaxRange());
+ }
+ } else if (praxiPlayer.isInMatch()) {
+ final Match match = praxiPlayer.getMatch();
+
+ if (match == null) {
+ return null;
+ }
+
+ if (match.isSoloMatch()) {
+ final MatchPlayer opponent = match.getOpponentMatchPlayer(player);
+
+ toReturn.add(Style.YELLOW + "Opponent: " + Style.PINK + opponent.getName());
+ toReturn.add(Style.YELLOW + "Duration: " + Style.PINK + match.getDuration());
+
+ if (match.isFighting()) {
+ toReturn.add("");
+ toReturn.add(Style.YELLOW + "Your Ping: " + Style.PINK + player.getPing() + "ms");
+ toReturn.add(Style.YELLOW + "Their Ping: " + Style.PINK + opponent.getPing() + "ms");
+ }
+ } else if (match.isTeamMatch()) {
+ toReturn.add(Style.YELLOW + "Duration: " + Style.PINK + match.getDuration());
+ toReturn.add(Style.YELLOW + "Opponents: " + Style.PINK + match.getOpponentsLeft(player) + "/" +
+ match.getOpponentTeam(player).getTeamPlayers().size());
+
+ if (match.getTeam(player).getTeamPlayers().size() >= 8) {
+ toReturn.add(Style.YELLOW + "Your Team: " + Style.PINK + match.getTeam(player).getTeamPlayers().size());
+ } else {
+ toReturn.add("");
+ toReturn.add(Style.YELLOW + "Your Team:");
+
+ match.getTeam(player).getTeamPlayers().forEach(teamPlayer -> {
+ toReturn.add(" " + (teamPlayer.isDisconnected() || !teamPlayer.isAlive() ? Style.STRIKE_THROUGH
+ : "") + teamPlayer.getName());
+ });
+ }
+ }
+ } else if (praxiPlayer.isSpectating()) {
+ final Match match = praxiPlayer.getMatch();
+
+ toReturn.add(Style.YELLOW + "Ladder: " + Style.PINK + match.getLadder().getName());
+ toReturn.add(Style.YELLOW + "Duration: " + Style.PINK + match.getDuration());
+ toReturn.add(Style.YELLOW + "Players:");
+
+ if (match.isSoloMatch()) {
+ toReturn.add(" " + match.getMatchPlayerA().getName() + Style.GRAY + " (" + match.getMatchPlayerA().getPing() + ")");
+ toReturn.add(" " + match.getMatchPlayerB().getName() + Style.GRAY + " (" + match.getMatchPlayerB().getPing() + ")");
+ } else {
+ toReturn.add(" " + match.getTeamA().getLeader().getName() + "'s Team");
+ toReturn.add(" " + match.getTeamA().getLeader().getName() + "'s Team");
+ }
+ } else if (praxiPlayer.isInEvent()) {
+ final Event event = praxiPlayer.getEvent();
+
+ toReturn.add(Style.YELLOW + "Event: " + Style.PINK + "Sumo");
+
+ if (event.isWaiting()) {
+ toReturn.add(Style.YELLOW + "Players: " + Style.PINK + event.getEventPlayers().size() + "/" + event.getMaxPlayers());
+ toReturn.add("");
+
+ if (event.getCooldown() == null) {
+ toReturn.add(Style.GRAY + Style.ITALIC + "Waiting for players...");
+ } else {
+ toReturn.add(Style.GRAY + Style.ITALIC + "Starting in " +
+ TimeUtil.millisToSeconds(event.getCooldown().getRemaining()) + "s");
+ }
+ } else {
+ toReturn.add(Style.YELLOW + "Remaining: " + Style.PINK + event.getRemainingPlayers() + "/" + event.getMaxPlayers());
+ toReturn.add(Style.YELLOW + "Duration: " + Style.PINK + event.getRoundDuration());
+ toReturn.add(Style.YELLOW + "Players:");
+ toReturn.add(" " + event.getRoundPlayerA().getName() + Style.GRAY + " (" + event.getRoundPlayerA().getPing() + " ms)");
+ toReturn.add(" " + event.getRoundPlayerB().getName() + Style.GRAY + " (" + event.getRoundPlayerB().getPing() + " ms)");
+ }
+ }
+
+ toReturn.add(0, Style.BORDER_LINE_SCOREBOARD);
+ toReturn.add("");
+ toReturn.add(Style.PINK + "minexd.com");
+ toReturn.add(Style.BORDER_LINE_SCOREBOARD);
+
+ return toReturn;
+ }
+
+ @Override
+ public long getInterval() {
+ return 2L;
+ }
+
+ @Override
+ public void preLoop() {
+ }
+
+ @Override
+ public void onScoreboardCreate(Player player, Scoreboard scoreboard) {
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/ArenaCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/ArenaCommands.java
new file mode 100644
index 0000000..a94a607
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/ArenaCommands.java
@@ -0,0 +1,207 @@
+package me.joeleoli.praxi.command;
+
+import java.io.File;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.CommandHelp;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.arena.ArenaType;
+import me.joeleoli.praxi.arena.SharedArena;
+import me.joeleoli.praxi.arena.StandaloneArena;
+import me.joeleoli.praxi.arena.generator.ArenaGenerator;
+import me.joeleoli.praxi.arena.generator.Schematic;
+import me.joeleoli.praxi.arena.selection.Selection;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.block.Sign;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class ArenaCommands {
+
+ private static final CommandHelp[] HELP = new CommandHelp[]{
+ new CommandHelp("/arena list", "List all arenas"),
+ new CommandHelp("/arena create ", "Create an arena"),
+ new CommandHelp("/arena delete ", "Delete an arena"),
+ new CommandHelp("/arena setspawn <1:2> ", "Set a spawn point"),
+ };
+
+ @Command(names = { "arena", "arena help" }, permissionNode = "praxi.arena")
+ public static void help(Player player) {
+ for (CommandHelp help : HELP) {
+ player.sendMessage(
+ Style.YELLOW + help.getSyntax() + Style.GRAY + " - " + Style.PINK + help.getDescription());
+ }
+ }
+
+ @Command(names = "arena wand", permissionNode = "praxi.arena")
+ public static void wand(Player player) {
+ player.getInventory().addItem(Selection.SELECTION_WAND);
+ player.sendMessage(Style.YELLOW + "You have been given the selection wand.");
+ }
+
+ @Command(names = "arena save", permissionNode = "praxi.arena")
+ public static void save(CommandSender sender) {
+ Arena.getArenas().forEach(Arena::save);
+ sender.sendMessage(Style.GREEN + "Saved all arenas.");
+ }
+
+ @Command(names = "arena list", permissionNode = "praxi.arena")
+ public static void list(Player player) {
+ player.sendMessage(Style.GOLD + "Arenas:");
+
+ if (Arena.getArenas().isEmpty()) {
+ player.sendMessage(Style.GRAY + "There are no arenas.");
+ return;
+ }
+
+ for (Arena arena : Arena.getArenas()) {
+ if (arena.getType() != ArenaType.DUPLICATE) {
+ player.sendMessage(Style.GRAY + " - " + (arena.isSetup() ? Style.GREEN : Style.RED) + arena.getName() +
+ Style.GRAY + " (" + arena.getType().name() + ")");
+ }
+ }
+ }
+
+ @Command(names = "arena create", permissionNode = "praxi.arena")
+ public static void create(Player player, @Parameter(name = "name") String name,
+ @Parameter(name = "type") ArenaType type) {
+ Arena arena = Arena.getByName(name);
+
+ if (arena != null) {
+ player.sendMessage(Style.RED + "An arena with that name already exists.");
+ return;
+ }
+
+ Selection selection = Selection.createOrGetSelection(player);
+
+ if (!selection.isFullObject()) {
+ player.sendMessage(Style.RED + "You must have a full selection to create an arena.");
+ return;
+ }
+
+ if (type == ArenaType.STANDALONE) {
+ arena = new StandaloneArena(name, selection.getPoint1(), selection.getPoint2());
+ } else {
+ arena = new SharedArena(name, selection.getPoint1(), selection.getPoint2());
+ }
+
+ arena.save();
+
+ Arena.getArenas().add(arena);
+
+ player.sendMessage(Style.GREEN + "Arena `" + arena.getName() + "` has been created.");
+ }
+
+ @Command(names = "arena delete", permissionNode = "praxi.arena")
+ public static void delete(Player player, @Parameter(name = "arena") Arena arena) {
+ arena.delete();
+
+ Arena.getArenas().remove(arena);
+
+ if (arena instanceof StandaloneArena) {
+ Arena.getArenas().removeAll(((StandaloneArena) arena).getDuplicates());
+ }
+
+ player.sendMessage(Style.GREEN + "Arena `" + arena.getName() + "` has been deleted.");
+ }
+
+ @Command(names = "arena setspawn", permissionNode = "praxi.arena")
+ public static void setSpawn(Player player, @Parameter(name = "loc") int loc,
+ @Parameter(name = "arena") Arena arena) {
+ if (loc == 1) {
+ arena.setSpawn1(player.getLocation());
+ } else if (loc == 2) {
+ arena.setSpawn2(player.getLocation());
+ } else {
+ player.sendMessage(Style.RED + "Choose position `1` or position `2`.");
+ return;
+ }
+
+ arena.save();
+
+ player.sendMessage(Style.GREEN + "You set the spawn position " + loc + (loc == 1 ? "st" : "nd") + " for `" +
+ arena.getName() + "`.");
+ }
+
+ @Command(names = "arena generate", permissionNode = "praxi.arena.generate", async = true)
+ public static void generate(Player player) {
+ File schematicsFolder = new File(Praxi.getInstance().getDataFolder().getPath() + File.separator + "schematics");
+
+ if (!schematicsFolder.exists()) {
+ player.sendMessage(Style.RED + "The schematics folder does not exist.");
+ return;
+ }
+
+ for (File file : schematicsFolder.listFiles()) {
+ if (!file.isDirectory()) {
+ if (file.getName().contains(".schematic")) {
+ final boolean duplicate = file.getName().endsWith("_duplicate.schematic");
+
+ final String name = file.getName()
+ .replace(".schematic", "")
+ .replace("_duplicate", "");
+
+ final Arena parent = Arena.getByName(name);
+
+ if (parent != null) {
+ if (!(parent instanceof StandaloneArena)) {
+ continue;
+ }
+ }
+
+ TaskUtil.run(() -> {
+ try {
+ new ArenaGenerator(name, Bukkit.getWorlds().get(0), new Schematic(file),
+ duplicate ? (parent != null ? ArenaType.DUPLICATE : ArenaType.STANDALONE)
+ : ArenaType.SHARED
+ ).generate(file, (StandaloneArena) parent);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ });
+ }
+ }
+ }
+
+ player.sendMessage(Style.GREEN + "Generating arenas... See console for details.");
+ }
+
+ @Command(names = "arena tp", permissionNode = "praxi.arena")
+ public static void teleport(Player player, @Parameter(name = "arena") Arena arena) {
+ if (arena.getSpawn1() != null) {
+ player.teleport(arena.getSpawn1());
+ } else if (arena.getSpawn2() != null) {
+ player.teleport(arena.getSpawn2());
+ } else {
+ player.teleport(arena.getUpperCorner());
+ }
+
+ player.sendMessage(Style.GREEN + "You teleported to " + Style.AQUA + arena.getName() + Style.GREEN + ".");
+ }
+
+ @Command(names = "arena genhelper", permissionNode = "praxi.arena.genhelp")
+ public static void generatorHelper(Player player) {
+ final Block origin = player.getLocation().getBlock();
+ final Block up = origin.getRelative(BlockFace.UP);
+
+ origin.setType(Material.SPONGE);
+ up.setType(Material.SIGN_POST);
+
+ if (up.getState() instanceof Sign) {
+ final Sign sign = (Sign) up.getState();
+
+ sign.setLine(0, ((int) player.getLocation().getPitch()) + "");
+ sign.setLine(1, ((int) player.getLocation().getYaw()) + "");
+ sign.update();
+
+ player.sendMessage(Style.GREEN + "Generator helper placed.");
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/DuelCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/DuelCommands.java
new file mode 100644
index 0000000..133bdc3
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/DuelCommands.java
@@ -0,0 +1,158 @@
+package me.joeleoli.praxi.command;
+
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.duel.DuelProcedure;
+import me.joeleoli.praxi.duel.DuelRequest;
+import me.joeleoli.praxi.duel.gui.DuelSelectLadderMenu;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.impl.SoloMatch;
+import me.joeleoli.praxi.player.PracticeSetting;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.player.RematchData;
+import org.bukkit.entity.Player;
+
+public class DuelCommands {
+
+ @Command(names = "duel")
+ public static void duel(Player player, @Parameter(name = "target") Player target) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot duel while frozen.");
+ return;
+ }
+
+ if (NucleusAPI.isFrozen(target)) {
+ player.sendMessage(Style.RED + "You cannot duel a frozen player.");
+ return;
+ }
+
+ if (player.getUniqueId().equals(target.getUniqueId())) {
+ player.sendMessage(Style.RED + "You cannot duel yourself.");
+ return;
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final PraxiPlayer targetData = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (praxiPlayer.isBusy()) {
+ player.sendMessage(Style.RED + "You cannot duel right now.");
+ return;
+ }
+
+ if (targetData.isBusy()) {
+ player.sendMessage(NucleusAPI.getColoredName(target) + Style.RED + " is currently busy.");
+ return;
+ }
+
+ if (!NucleusAPI.getSetting(target, PracticeSetting.RECEIVE_DUEL_REQUESTS)) {
+ player.sendMessage(Style.RED + "That player is not accepting duel requests at the moment.");
+ return;
+ }
+
+ if (!praxiPlayer.canSendDuelRequest(player)) {
+ player.sendMessage(Style.RED + "You have already sent that player a duel request.");
+ return;
+ }
+
+ DuelProcedure procedure = new DuelProcedure();
+
+ procedure.setSender(player);
+ procedure.setTarget(target);
+
+ praxiPlayer.setDuelProcedure(procedure);
+
+ new DuelSelectLadderMenu().openMenu(player);
+ }
+
+ @Command(names = "duel accept")
+ public static void accept(Player player, @Parameter(name = "target") Player target) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot duel while frozen.");
+ return;
+ }
+
+ if (NucleusAPI.isFrozen(target)) {
+ player.sendMessage(Style.RED + "You cannot duel a frozen player.");
+ return;
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final PraxiPlayer targetData = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (!targetData.isPendingDuelRequest(player)) {
+ player.sendMessage(Style.RED + "You do not have a pending duel request from " + NucleusAPI.getColoredName(target) + Style.RED + ".");
+ return;
+ }
+
+ if (praxiPlayer.isBusy()) {
+ player.sendMessage(Style.RED + "You cannot duel right now.");
+ return;
+ }
+
+ if (targetData.isBusy()) {
+ player.sendMessage(NucleusAPI.getColoredName(target) + Style.RED + " is currently busy.");
+ return;
+ }
+
+ final DuelRequest request = targetData.getSentDuelRequests().get(player.getUniqueId());
+
+ Arena arena = request.getArena();
+
+ if (arena.isActive()) {
+ arena = Arena.getRandom(request.getLadder());
+ }
+
+ if (arena == null) {
+ player.sendMessage(Style.RED + "Tried to start a match but there are no available arenas.");
+ return;
+ }
+
+ arena.setActive(true);
+
+ Match match = new SoloMatch(new MatchPlayer(player), new MatchPlayer(target), request.getLadder(), arena, false,
+ true
+ );
+
+ match.handleStart();
+ }
+
+ @Command(names = "rematch")
+ public static void rematch(Player player) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot duel while frozen.");
+ return;
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getRematchData() == null) {
+ player.sendMessage(Style.RED + "You do not have anyone to re-match.");
+ return;
+ }
+
+ praxiPlayer.refreshHotbar();
+
+ if (praxiPlayer.getRematchData() == null) {
+ player.sendMessage(Style.RED + "That player is no longer available.");
+ return;
+ }
+
+ final RematchData rematchData = praxiPlayer.getRematchData();
+
+ if (rematchData.isReceive()) {
+ rematchData.accept();
+ } else {
+ if (rematchData.isSent()) {
+ player.sendMessage(Style.RED + "You have already sent a rematch request to that player.");
+ return;
+ }
+
+ rematchData.request();
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/EventCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/EventCommands.java
new file mode 100644
index 0000000..edb452a
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/EventCommands.java
@@ -0,0 +1,127 @@
+package me.joeleoli.praxi.command;
+
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.events.impl.SumoEvent;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class EventCommands {
+
+ @Command(names = "eventmanager cancel", permissionNode = "praxi.event.admin")
+ public static void cancel(CommandSender sender) {
+ if (Praxi.getInstance().getEventManager().getActiveEvent() == null) {
+ sender.sendMessage(Style.RED + "There is no active event.");
+ return;
+ }
+
+ Praxi.getInstance().getEventManager().getActiveEvent().end();
+ }
+
+ @Command(names = "eventmanager cooldown", permissionNode = "praxi.event.admin")
+ public static void cooldown(CommandSender sender) {
+ if (Praxi.getInstance().getEventManager().getEventCooldown().hasExpired()) {
+ sender.sendMessage(Style.RED + "There is no event cooldown active.");
+ return;
+ }
+
+ sender.sendMessage(Style.GREEN + "You reset the event cooldown.");
+ Praxi.getInstance().getEventManager().setEventCooldown(new Cooldown(0));
+ }
+
+ @Command(names = "eventmanager setspawn pos", permissionNode = "praxi.event.admin")
+ public static void setSpawnPosition(Player player, @Parameter(name = "pos") int position) {
+ if (!(position == 1 || position == 2)) {
+ player.sendMessage(Style.RED + "The position must be 1 or 2.");
+ } else {
+ if (position == 1) {
+ Praxi.getInstance().getEventManager().setSumoSpawn1(player.getLocation());
+ } else {
+ Praxi.getInstance().getEventManager().setSumoSpawn2(player.getLocation());
+ }
+
+ Praxi.getInstance().getEventManager().save();
+ player.sendMessage(Style.GREEN + "Updated event's spawn location " + position + ".");
+ }
+ }
+
+ @Command(names = "eventmanager setspawn spec", permissionNode = "praxi.event.admin")
+ public static void setSpawnSpectator(Player player) {
+ Praxi.getInstance().getEventManager().setSumoSpectator(player.getLocation());
+ Praxi.getInstance().getEventManager().save();
+ player.sendMessage(Style.GREEN + "Updated event's spawn spectator location.");
+ }
+
+ @Command(names = { "event host", "host" }, permissionNode = "praxi.event.host")
+ public static void hostEvent(Player player) {
+ if (Praxi.getInstance().getEventManager().getActiveEvent() != null) {
+ player.sendMessage(Style.RED + "There is already an active event.");
+ return;
+ }
+
+ if (!Praxi.getInstance().getEventManager().getEventCooldown().hasExpired()) {
+ player.sendMessage(Style.RED + "There is an event cooldown active.");
+ return;
+ }
+
+ Praxi.getInstance().getEventManager().setActiveEvent(new SumoEvent(player));
+
+ for (Player other : Praxi.getInstance().getServer().getOnlinePlayers()) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(other.getUniqueId());
+
+ if (praxiPlayer.isInLobby()) {
+ if (!praxiPlayer.getKitEditor().isActive()) {
+ praxiPlayer.loadHotbar();
+ }
+ }
+ }
+ }
+
+ @Command(names = { "event join" })
+ public static void eventJoin(Player player) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final Event activeEvent = Praxi.getInstance().getEventManager().getActiveEvent();
+
+ if (praxiPlayer.isBusy()) {
+ player.sendMessage(Style.RED + "You cannot join the event right now.");
+ return;
+ }
+
+ if (activeEvent == null) {
+ player.sendMessage(Style.RED + "There is no active event.");
+ return;
+ }
+
+ if (activeEvent.getState() != EventState.WAITING) {
+ player.sendMessage(Style.RED + "That event is currently on-going and cannot be joined.");
+ return;
+ }
+
+ Praxi.getInstance().getEventManager().getActiveEvent().handleJoin(player);
+ }
+
+ @Command(names = { "event leave" })
+ public static void eventLeave(Player player) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final Event activeEvent = Praxi.getInstance().getEventManager().getActiveEvent();
+
+ if (activeEvent == null) {
+ player.sendMessage(Style.RED + "There is no active event.");
+ return;
+ }
+
+ if (!praxiPlayer.isInEvent() || !activeEvent.getEventPlayers().containsKey(player.getUniqueId())) {
+ player.sendMessage(Style.RED + "You are not apart of the active event.");
+ return;
+ }
+
+ Praxi.getInstance().getEventManager().getActiveEvent().handleLeave(player);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/LadderCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/LadderCommands.java
new file mode 100644
index 0000000..6e7dc0c
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/LadderCommands.java
@@ -0,0 +1,109 @@
+package me.joeleoli.praxi.command;
+
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.ragespigot.RageSpigot;
+import me.joeleoli.ragespigot.knockback.KnockbackProfile;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class LadderCommands {
+
+ @Command(names = "ladder enable", permissionNode = "praxi.ladder")
+ public static void enable(CommandSender sender, @Parameter(name = "ladder") Ladder ladder) {
+ ladder.setEnabled(true);
+ sender.sendMessage(Style.GREEN + "You enabled the " + ladder.getDisplayName() + Style.GREEN + " ladder.");
+ }
+
+ @Command(names = "ladder disable", permissionNode = "praxi.ladder")
+ public static void disable(CommandSender sender, @Parameter(name = "ladder") Ladder ladder) {
+ ladder.setEnabled(false);
+ sender.sendMessage(Style.GREEN + "You disabled the " + ladder.getDisplayName() + Style.GREEN + " ladder.");
+ }
+
+ @Command(names = "ladder sethitdelay", permissionNode = "praxi.ladder")
+ public static void setHitDelay(CommandSender sender, @Parameter(name = "ladder") Ladder ladder,
+ @Parameter(name = "hitdelay") int hitDelay) {
+ if (hitDelay < 0 || hitDelay > 20) {
+ sender.sendMessage(Style.RED + "The hit delay must be in the range of 0-20.");
+ return;
+ }
+
+ ladder.setHitDelay(hitDelay);
+ sender.sendMessage(Style.GREEN + "You set the hit delay of " + ladder.getDisplayName() + Style.GREEN + " to: " +
+ Style.RESET + ladder.getHitDelay());
+ }
+
+ @Command(names = "ladder list", permissionNode = "praxi.ladder")
+ public static void list(CommandSender sender) {
+ sender.sendMessage(Style.GOLD + Style.BOLD + "Ladders:");
+
+ Ladder.getLadders().forEach(ladder -> {
+ sender.sendMessage(Style.GRAY + " - " + ladder.getDisplayName());
+ });
+ }
+
+ @Command(names = "ladder create", permissionNode = "praxi.ladder")
+ public static void create(Player player, @Parameter(name = "name") String name) {
+ Ladder ladder = Ladder.getByName(name);
+
+ if (ladder != null) {
+ player.sendMessage(Style.RED + "A ladder with that name already exists.");
+ return;
+ }
+
+ ladder = new Ladder(name);
+ ladder.save();
+
+ player.sendMessage(
+ Style.GREEN + "Created a new ladder named " + Style.AQUA + ladder.getName() + Style.GREEN + ".");
+ }
+
+ @Command(names = "ladder setkit", permissionNode = "praxi.ladder")
+ public static void setKit(Player player, @Parameter(name = "ladder") Ladder ladder) {
+ ladder.getDefaultKit().setArmor(player.getInventory().getArmorContents());
+ ladder.getDefaultKit().setContents(player.getInventory().getContents());
+ ladder.save();
+
+ player.sendMessage(Style.GREEN + "Updated " + Style.AQUA + ladder.getName() + Style.GREEN + "'s default kit.");
+ }
+
+ @Command(names = "ladder loadkit", permissionNode = "praxi.ladder")
+ public static void loadKit(Player player, @Parameter(name = "ladder") Ladder ladder) {
+ player.getInventory().setArmorContents(ladder.getDefaultKit().getArmor());
+ player.getInventory().setContents(ladder.getDefaultKit().getContents());
+ player.updateInventory();
+ player.sendMessage(Style.GREEN + "Loaded " + Style.AQUA + ladder.getName() + Style.GREEN + "'s default kit.");
+ }
+
+ @Command(names = "ladder setdisplayname", permissionNode = "praxi.ladder")
+ public static void setDisplayName(CommandSender sender, @Parameter(name = "ladder") Ladder ladder,
+ @Parameter(name = "displayName") String displayName) {
+ ladder.setDisplayName(Style.translate(displayName));
+ ladder.save();
+
+ sender.sendMessage(
+ Style.GREEN + "You set " + Style.AQUA + ladder.getName() + "'s display name " + Style.GREEN + "to: " +
+ Style.AQUA + Style.translate(displayName));
+ }
+
+ @Command(names = "ladder setkbprofile", permissionNode = "praxi.ladder")
+ public static void setKnockbackProfile(CommandSender sender, @Parameter(name = "ladder") Ladder ladder,
+ @Parameter(name = "profile") String profileName) {
+ final KnockbackProfile profile = RageSpigot.INSTANCE.getConfig().getKbProfileByName(profileName);
+
+ if (profile == null) {
+ sender.sendMessage(Style.RED + "A knockback profile with that name could not be found.");
+ return;
+ }
+
+ ladder.setKbProfile(profileName);
+ ladder.save();
+
+ sender.sendMessage(
+ Style.GREEN + "You set the kb-profile for " + ladder.getDisplayName() + Style.GREEN + " ladder.");
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/ManagementCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/ManagementCommands.java
new file mode 100644
index 0000000..e1f1256
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/ManagementCommands.java
@@ -0,0 +1,68 @@
+package me.joeleoli.praxi.command;
+
+import java.util.UUID;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.CommandHelp;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.nucleus.uuid.UUIDCache;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class ManagementCommands {
+
+ private static final CommandHelp[] HELP = new CommandHelp[]{
+ new CommandHelp("/praxi reload", "Reload the config"),
+ };
+
+ @Command(names = { "praxi", "praxi help" }, permissionNode = "praxi.admin")
+ public static void help(Player player) {
+ for (CommandHelp help : HELP) {
+ player.sendMessage(
+ Style.YELLOW + help.getSyntax() + Style.GRAY + " - " + Style.PINK + help.getDescription());
+ }
+ }
+
+ @Command(names = "resetelo", permissionNode = "prax.admin.resetelo")
+ public static void resetElo(CommandSender sender, @Parameter(name = "target") String targetName) {
+ UUID uuid;
+
+ try {
+ uuid = UUID.fromString(targetName);
+ } catch (Exception e) {
+ uuid = UUIDCache.getUuid(targetName);
+ }
+
+ if (uuid == null) {
+ sender.sendMessage(
+ Style.RED + "Couldn't find a player with the name " + Style.RESET + targetName + Style.RED +
+ ". Have they joined the network?");
+ return;
+ }
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(uuid);
+
+ if (praxiPlayer.isLoaded()) {
+ praxiPlayer.getStatistics().getLadders().values().forEach(stats -> {
+ stats.setElo(1000);
+ });
+
+ praxiPlayer.save();
+ } else {
+ TaskUtil.runAsync(() -> {
+ praxiPlayer.load();
+
+ praxiPlayer.getStatistics().getLadders().values().forEach(stats -> {
+ stats.setElo(1000);
+ });
+
+ praxiPlayer.save();
+ });
+ }
+
+ sender.sendMessage(Style.GREEN + "You reset " + targetName + "'s elo.");
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/MatchCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/MatchCommands.java
new file mode 100644
index 0000000..31d3ec2
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/MatchCommands.java
@@ -0,0 +1,31 @@
+package me.joeleoli.praxi.command;
+
+import java.util.UUID;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.match.MatchSnapshot;
+import me.joeleoli.praxi.match.gui.MatchDetailsMenu;
+import org.bukkit.entity.Player;
+
+public class MatchCommands {
+
+ @Command(names = { "viewinventory", "viewinv" })
+ public static void viewInventory(Player player, @Parameter(name = "id") String id) {
+ MatchSnapshot cachedInventory;
+
+ try {
+ cachedInventory = MatchSnapshot.getByUuid(UUID.fromString(id));
+ } catch (Exception e) {
+ cachedInventory = MatchSnapshot.getByName(id);
+ }
+
+ if (cachedInventory == null) {
+ player.sendMessage(Style.RED + "Couldn't find an inventory for that ID.");
+ return;
+ }
+
+ new MatchDetailsMenu(cachedInventory).openMenu(player);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/PartyCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/PartyCommands.java
new file mode 100644
index 0000000..e32a8b3
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/PartyCommands.java
@@ -0,0 +1,254 @@
+package me.joeleoli.praxi.command;
+
+import java.util.UUID;
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.CommandHelp;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.party.Party;
+import me.joeleoli.praxi.party.PartyState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public class PartyCommands {
+
+ private static final CommandHelp[] HELP = new CommandHelp[]{
+ new CommandHelp("/party create", "Create a party"),
+ new CommandHelp("/party disband", "Disband your party"),
+ new CommandHelp("/party leave", "Leave your party"),
+ new CommandHelp("/party join ", "Join a party"),
+ new CommandHelp("/party kick ", "Kick a player from your party"),
+ new CommandHelp("/party open", "Make your party open"),
+ new CommandHelp("/party close", "Make your party closed"),
+ };
+
+ @Command(names = { "party", "party help" })
+ public static void help(Player player) {
+ for (CommandHelp help : HELP) {
+ player.sendMessage(
+ Style.YELLOW + help.getSyntax() + Style.GRAY + " - " + Style.PINK + help.getDescription());
+ }
+ }
+
+ @Command(names = { "p create", "party create" })
+ public static void create(Player player) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot create a party while frozen.");
+ return;
+ }
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() != null) {
+ player.sendMessage(Style.RED + "You already have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.isInLobby()) {
+ player.sendMessage(Style.RED + "You must be in the lobby to create a party.");
+ return;
+ }
+
+ praxiPlayer.setParty(new Party(player));
+ praxiPlayer.loadHotbar();
+
+ player.sendMessage(Style.YELLOW + "You created a new party.");
+ }
+
+ @Command(names = { "p disband", "party disband" })
+ public static void disband(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().isLeader(player.getUniqueId())) {
+ player.sendMessage(Style.RED + "You are not the leader of your party.");
+ return;
+ }
+
+ praxiPlayer.getParty().disband();
+ }
+
+ @Command(names = { "p invite", "party invite" })
+ public static void invite(Player player, @Parameter(name = "target") Player target) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().canInvite(target)) {
+ player.sendMessage(Style.RED + "That player has already been invited to your party.");
+ return;
+ }
+
+ if (praxiPlayer.getParty().containsPlayer(target)) {
+ player.sendMessage(Style.RED + "That player is already in your party.");
+ return;
+ }
+
+ if (praxiPlayer.getParty().getState() == PartyState.OPEN) {
+ player.sendMessage(Style.RED + "The party state is Open. You do not need to invite players.");
+ return;
+ }
+
+ final PraxiPlayer targetData = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (targetData.isBusy()) {
+ player.sendMessage(NucleusAPI.getColoredName(target) + Style.RED + " is currently busy.");
+ return;
+ }
+
+ praxiPlayer.getParty().invite(target);
+ }
+
+ @Command(names = { "p join", "party join" })
+ public static void join(Player player, @Parameter(name = "target") String targetId) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot join a party while frozen.");
+ return;
+ }
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() != null) {
+ player.sendMessage(Style.RED + "You already have a party.");
+ return;
+ }
+
+ Player target;
+
+ try {
+ target = Bukkit.getPlayer(UUID.fromString(targetId));
+ } catch (Exception e) {
+ target = Bukkit.getPlayer(targetId);
+ }
+
+ if (target == null) {
+ player.sendMessage(Style.RED + "A player with that name could not be found.");
+ return;
+ }
+
+ PraxiPlayer targetData = PraxiPlayer.getByUuid(target.getUniqueId());
+ Party party = targetData.getParty();
+
+ if (party == null) {
+ player.sendMessage(Style.RED + "A party with that name could not be found.");
+ return;
+ }
+
+ if (party.getState() == PartyState.CLOSED) {
+ if (!party.isInvited(player)) {
+ player.sendMessage(Style.RED + "You have not been invited to that party.");
+ return;
+ }
+ }
+
+ if (party.getPlayers().size() >= 32) {
+ player.sendMessage(Style.RED + "That party is full and cannot hold anymore players.");
+ return;
+ }
+
+ party.join(player);
+ }
+
+ @Command(names = { "p leave", "party leave" })
+ public static void leave(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (praxiPlayer.getParty().getLeader().getUuid().equals(player.getUniqueId())) {
+ praxiPlayer.getParty().disband();
+ } else {
+ praxiPlayer.getParty().leave(player, false);
+ }
+ }
+
+ @Command(names = { "p kick", "party kick" })
+ public static void kick(Player player, @Parameter(name = "target") Player target) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().isLeader(player.getUniqueId())) {
+ player.sendMessage(Style.RED + "You are not the leader of your party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().containsPlayer(target)) {
+ player.sendMessage(Style.RED + "That player is not a member of your party.");
+ return;
+ }
+
+ if (player.equals(target)) {
+ player.sendMessage(Style.RED + "You cannot kick yourself from your party.");
+ return;
+ }
+
+ praxiPlayer.getParty().leave(target, true);
+ }
+
+ @Command(names = { "p close", "party close" })
+ public static void open(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().isLeader(player.getUniqueId())) {
+ player.sendMessage(Style.RED + "You are not the leader of your party.");
+ return;
+ }
+
+ praxiPlayer.getParty().setState(PartyState.CLOSED);
+ }
+
+ @Command(names = { "p open", "party open" })
+ public static void close(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ if (!praxiPlayer.getParty().isLeader(player.getUniqueId())) {
+ player.sendMessage(Style.RED + "You are not the leader of your party.");
+ return;
+ }
+
+ praxiPlayer.getParty().setState(PartyState.OPEN);
+ }
+
+ @Command(names = { "p info", "party info", "party information" })
+ public static void information(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (!praxiPlayer.isLoaded()) {
+ return;
+ }
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You do not have a party.");
+ return;
+ }
+
+ praxiPlayer.getParty().sendInformation(player);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/PlayerCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/PlayerCommands.java
new file mode 100644
index 0000000..c2107d5
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/PlayerCommands.java
@@ -0,0 +1,73 @@
+package me.joeleoli.praxi.command;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.uuid.UUIDCache;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.player.gui.PlayerSettingsMenu;
+import org.bukkit.entity.Player;
+
+public class PlayerCommands {
+
+ @Command(names = "fly", permissionNode = "praxi.donor.fly")
+ public static void fly(Player player) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInLobby() || praxiPlayer.isInQueue()) {
+ player.setAllowFlight(true);
+ player.setFlying(true);
+ player.updateInventory();
+ player.sendMessage(Style.YELLOW + "You are now flying.");
+ } else {
+ player.sendMessage(Style.RED + "You cannot fly right now.");
+ }
+ }
+
+ @Command(names = { "settings", "options" })
+ public static void settings(Player player) {
+ new PlayerSettingsMenu().openMenu(player);
+ }
+
+ @Command(names = { "statistics", "stats" }, async = true)
+ public static void statistics(Player player, @Parameter(name = "target", defaultValue = "self") String name) {
+ if (name.equalsIgnoreCase("self")) {
+ name = player.getName();
+ }
+
+ final UUID uuid = UUIDCache.getUuid(name);
+
+ if (uuid == null) {
+ player.sendMessage(
+ Style.RED + "Couldn't find a player with the name " + Style.RESET + name + Style.RED + ".");
+ return;
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(uuid);
+
+ if (!praxiPlayer.isLoaded()) {
+ praxiPlayer.load();
+ }
+
+ if (praxiPlayer.getName() != null) {
+ if (praxiPlayer.getName().equalsIgnoreCase(name)) {
+ name = praxiPlayer.getName();
+ }
+ }
+
+ final List messages = new ArrayList<>();
+
+ praxiPlayer.getStatistics().getLadders().forEach((key, value) -> {
+ messages.add(Style.YELLOW + key + Style.GRAY + ": " + Style.PINK + value.getElo() + " ELO");
+ });
+
+ messages.add(0, Style.GOLD + Style.BOLD + name + "'s Statistics");
+ messages.add(0, Style.getBorderLine());
+ messages.add(Style.getBorderLine());
+ messages.forEach(player::sendMessage);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/SpectateCommands.java b/plugin/src/main/java/me/joeleoli/praxi/command/SpectateCommands.java
new file mode 100644
index 0000000..f429906
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/SpectateCommands.java
@@ -0,0 +1,58 @@
+package me.joeleoli.praxi.command;
+
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.command.Command;
+import me.joeleoli.nucleus.command.param.Parameter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.player.PracticeSetting;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+
+public class SpectateCommands {
+
+ @Command(names = { "spectate", "spec" })
+ public static void spectate(Player player, @Parameter(name = "target") Player target) {
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot spectate while frozen.");
+ return;
+ }
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ PraxiPlayer targetData = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (praxiPlayer.isBusy()) {
+ player.sendMessage(Style.RED + "You cannot spectate right now.");
+ return;
+ }
+
+ if (praxiPlayer.getParty() != null) {
+ player.sendMessage(Style.RED + "You must leave your party to spectate a match.");
+ return;
+ }
+
+ if (targetData == null || !targetData.isInMatch()) {
+ player.sendMessage(Style.RED + "That player is not in a match.");
+ return;
+ }
+
+ if (!NucleusAPI.getSetting(target, PracticeSetting.RECEIVE_DUEL_REQUESTS)) {
+ player.sendMessage(Style.RED + "That player is not allowing spectators.");
+ return;
+ }
+
+ targetData.getMatch().addSpectator(player, target);
+ }
+
+ @Command(names = "stopspectate")
+ public static void stopSpectate(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer == null || !praxiPlayer.isSpectating()) {
+ player.sendMessage(Style.RED + "You are not spectating a match.");
+ return;
+ }
+
+ praxiPlayer.getMatch().removeSpectator(player);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaParameterType.java b/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaParameterType.java
new file mode 100644
index 0000000..a6ca5ca
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaParameterType.java
@@ -0,0 +1,38 @@
+package me.joeleoli.praxi.command.param;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import me.joeleoli.nucleus.command.param.ParameterType;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import org.apache.commons.lang.StringUtils;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class ArenaParameterType implements ParameterType {
+
+ public Arena transform(CommandSender sender, String source) {
+ Arena arena = Arena.getByName(source);
+
+ if (arena == null) {
+ sender.sendMessage(Style.RED + "An arena with that name does not exist.");
+ return null;
+ }
+
+ return arena;
+ }
+
+ public List tabComplete(Player sender, Set flags, String source) {
+ List completions = new ArrayList<>();
+
+ for (Arena arena : Arena.getArenas()) {
+ if (arena.getName() != null && StringUtils.startsWithIgnoreCase(arena.getName(), source)) {
+ completions.add(arena.getName());
+ }
+ }
+
+ return completions;
+ }
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaTypeParameterType.java b/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaTypeParameterType.java
new file mode 100644
index 0000000..0d7004b
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/param/ArenaTypeParameterType.java
@@ -0,0 +1,37 @@
+package me.joeleoli.praxi.command.param;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import me.joeleoli.nucleus.command.param.ParameterType;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.ArenaType;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class ArenaTypeParameterType implements ParameterType {
+
+ public ArenaType transform(CommandSender sender, String source) {
+ ArenaType type;
+
+ try {
+ type = ArenaType.valueOf(source);
+ } catch (Exception e) {
+ sender.sendMessage(Style.RED + "That is not a valid arena type.");
+ return null;
+ }
+
+ return type;
+ }
+
+ public List tabComplete(Player sender, Set flags, String source) {
+ List completions = new ArrayList<>();
+
+ for (ArenaType type : ArenaType.values()) {
+ completions.add(type.name());
+ }
+
+ return completions;
+ }
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/param/LadderParameterType.java b/plugin/src/main/java/me/joeleoli/praxi/command/param/LadderParameterType.java
new file mode 100644
index 0000000..ebcbb36
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/param/LadderParameterType.java
@@ -0,0 +1,35 @@
+package me.joeleoli.praxi.command.param;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import me.joeleoli.nucleus.command.param.ParameterType;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.ladder.Ladder;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class LadderParameterType implements ParameterType {
+
+ public Ladder transform(CommandSender sender, String source) {
+ Ladder ladder = Ladder.getByName(source);
+
+ if (ladder == null) {
+ sender.sendMessage(Style.RED + "That is not a valid ladder type.");
+ return null;
+ }
+
+ return ladder;
+ }
+
+ public List tabComplete(Player sender, Set flags, String source) {
+ List completions = new ArrayList<>();
+
+ for (Ladder ladder : Ladder.getLadders()) {
+ completions.add(ladder.getName());
+ }
+
+ return completions;
+ }
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/command/param/QueueParameterType.java b/plugin/src/main/java/me/joeleoli/praxi/command/param/QueueParameterType.java
new file mode 100644
index 0000000..313cc6e
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/command/param/QueueParameterType.java
@@ -0,0 +1,35 @@
+package me.joeleoli.praxi.command.param;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import me.joeleoli.nucleus.command.param.ParameterType;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.queue.Queue;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+
+public class QueueParameterType implements ParameterType {
+
+ public Queue transform(CommandSender sender, String source) {
+ try {
+ Queue queue = Queue.getByUuid(UUID.fromString(source));
+
+ if (queue == null) {
+ sender.sendMessage(Style.RED + "A queue with that ID does not exist.");
+ return null;
+ }
+
+ return queue;
+ } catch (Exception e) {
+ sender.sendMessage(Style.RED + "A queue with that ID does not exist.");
+ return null;
+ }
+ }
+
+ public List tabComplete(Player sender, Set flags, String source) {
+ return Collections.emptyList();
+ }
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/config/ConfigItem.java b/plugin/src/main/java/me/joeleoli/praxi/config/ConfigItem.java
new file mode 100644
index 0000000..a0fb1e1
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/config/ConfigItem.java
@@ -0,0 +1,62 @@
+package me.joeleoli.praxi.config;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import me.joeleoli.nucleus.config.ConfigCursor;
+import me.joeleoli.nucleus.util.Style;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+@Getter
+public class ConfigItem {
+
+ private Material material = Material.AIR;
+ private short durability = 0;
+ private String name;
+ private List lore = new ArrayList<>();
+ private int amount = 1;
+
+ public ConfigItem(ConfigCursor cursor, String path) {
+ if (cursor.exists(path + ".material")) {
+ this.material = Material.valueOf(cursor.getString(path + ".material"));
+ }
+
+ if (cursor.exists(path + ".durability")) {
+ this.durability = (short) cursor.getInt(path + ".durability");
+ }
+
+ if (cursor.exists(path + ".name")) {
+ this.name = Style.translate(cursor.getString(path + ".name"));
+ }
+
+ if (cursor.exists(path + ".lore")) {
+ this.lore = Style.translateLines(cursor.getStringList(path + ".lore"));
+ }
+
+ if (cursor.exists(path + ".amount")) {
+ this.amount = cursor.getInt(path + ".amount");
+ }
+ }
+
+ public ItemStack toItemStack() {
+ ItemStack itemStack = new ItemStack(this.material);
+ ItemMeta itemMeta = itemStack.getItemMeta();
+
+ if (this.name != null) {
+ itemMeta.setDisplayName(Style.translate(this.name));
+ }
+
+ if (this.lore != null && !this.lore.isEmpty()) {
+ itemMeta.setLore(Style.translateLines(this.lore));
+ }
+
+ itemStack.setAmount(this.amount);
+ itemStack.setDurability(this.durability);
+ itemStack.setItemMeta(itemMeta);
+
+ return itemStack;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/cuboid/Cuboid.java b/plugin/src/main/java/me/joeleoli/praxi/cuboid/Cuboid.java
new file mode 100644
index 0000000..35d5595
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/cuboid/Cuboid.java
@@ -0,0 +1,519 @@
+package me.joeleoli.praxi.cuboid;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import lombok.Data;
+import org.bukkit.Bukkit;
+import org.bukkit.Chunk;
+import org.bukkit.Location;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+
+@Data
+public class Cuboid implements Iterable {
+
+ private String worldName;
+ private int x1, y1, z1;
+ private int x2, y2, z2;
+
+ /**
+ * Construct a Cuboid given two Location objects which represent any two corners of the Cuboid.
+ *
+ * @param l1 one of the corners
+ * @param l2 the other corner
+ */
+ public Cuboid(Location l1, Location l2) {
+ this(l1.getWorld().getName(),
+ l1.getBlockX(), l1.getBlockY(), l1.getBlockZ(),
+ l2.getBlockX(), l2.getBlockY(), l2.getBlockZ()
+ );
+
+ }
+
+ /**
+ * Construct a Cuboid in the given World and xyz coords
+ *
+ * @param world the Cuboid's world
+ * @param x1 X coord of corner 1
+ * @param y1 Y coord of corner 1
+ * @param z1 Z coord of corner 1
+ * @param x2 X coord of corner 2
+ * @param y2 Y coord of corner 2
+ * @param z2 Z coord of corner 2
+ */
+ public Cuboid(World world, int x1, int y1, int z1, int x2, int y2, int z2) {
+ this(world.getName(), x1, y1, z1, x2, y2, z2);
+ }
+
+ /**
+ * Construct a Cuboid in the given world name and xyz coords.
+ *
+ * @param worldName the Cuboid's world name
+ * @param x1 X coord of corner 1
+ * @param y1 Y coord of corner 1
+ * @param z1 Z coord of corner 1
+ * @param x2 X coord of corner 2
+ * @param y2 Y coord of corner 2
+ * @param z2 Z coord of corner 2
+ */
+ public Cuboid(String worldName, int x1, int y1, int z1, int x2, int y2, int z2) {
+ this.worldName = worldName;
+ this.x1 = Math.min(x1, x2);
+ this.x2 = Math.max(x1, x2);
+ this.y1 = Math.min(y1, y2);
+ this.y2 = Math.max(y1, y2);
+ this.z1 = Math.min(z1, z2);
+ this.z2 = Math.max(z1, z2);
+ }
+
+ /**
+ * Get the Location of the lower northeast corner of the Cuboid (minimum XYZ coords).
+ *
+ * @return Location of the lower northeast corner
+ */
+ public Location getLowerCorner() {
+ return new Location(getWorld(), x1, y1, z1);
+ }
+
+ /**
+ * Get the Location of the upper southwest corner of the Cuboid (maximum XYZ coords).
+ *
+ * @return Location of the upper southwest corner
+ */
+ public Location getUpperCorner() {
+ return new Location(getWorld(), x2, y2, z2);
+ }
+
+ /**
+ * Get the the center of the Cuboid
+ *
+ * @return Location at the centre of the Cuboid
+ */
+ public Location getCenter() {
+ return new Location(
+ getWorld(), getLowerX() + (getUpperX() - getLowerX()) / 2,
+ getLowerY() + (getUpperY() - getLowerY()) / 2, getLowerZ() + (getUpperZ() - getLowerZ()) / 2
+ );
+ }
+
+ /**
+ * Get the Cuboid's world.
+ *
+ * @return the World object representing this Cuboid's world
+ *
+ * @throws IllegalStateException if the world is not loaded
+ */
+ public World getWorld() {
+
+ World world = Bukkit.getWorld(worldName);
+ if (world == null) {
+ throw new IllegalStateException("world '" + worldName + "' is not loaded");
+ }
+ return world;
+ }
+
+ /**
+ * Get the size of this Cuboid along the X axis
+ *
+ * @return Size of Cuboid along the X axis
+ */
+ public int getSizeX() {
+ return (x2 - x1) + 1;
+ }
+
+ /**
+ * Get the size of this Cuboid along the Y axis
+ *
+ * @return Size of Cuboid along the Y axis
+ */
+ public int getSizeY() {
+ return (y2 - y1) + 1;
+ }
+
+ /**
+ * Get the size of this Cuboid along the Z axis
+ *
+ * @return Size of Cuboid along the Z axis
+ */
+ public int getSizeZ() {
+ return (z2 - z1) + 1;
+ }
+
+ /**
+ * Get the minimum X coord of this Cuboid
+ *
+ * @return the minimum X coord
+ */
+ public int getLowerX() {
+ return x1;
+ }
+
+ /**
+ * Get the minimum Y coord of this Cuboid
+ *
+ * @return the minimum Y coord
+ */
+ public int getLowerY() {
+ return y1;
+ }
+
+ /**
+ * Get the minimum Z coord of this Cuboid
+ *
+ * @return the minimum Z coord
+ */
+ public int getLowerZ() {
+ return z1;
+ }
+
+ /**
+ * Get the maximum X coord of this Cuboid
+ *
+ * @return the maximum X coord
+ */
+ public int getUpperX() {
+ return x2;
+ }
+
+ /**
+ * Get the maximum Y coord of this Cuboid
+ *
+ * @return the maximum Y coord
+ */
+ public int getUpperY() {
+ return y2;
+ }
+
+ /**
+ * Get the maximum Z coord of this Cuboid
+ *
+ * @return the maximum Z coord
+ */
+ public int getUpperZ() {
+ return z2;
+ }
+
+ /**
+ * Get the Blocks at the four corners of the Cuboid, without respect to y-value
+ *
+ * @return array of Block objects representing the Cuboid corners
+ */
+ public Location[] getCorners() {
+ Location[] res = new Location[4];
+ World w = getWorld();
+ res[0] = new Location(w, x1, 0, z1); // ++x
+ res[1] = new Location(w, x2, 0, z1); // ++z
+ res[2] = new Location(w, x2, 0, z2); // --x
+ res[3] = new Location(w, x1, 0, z2); // --z
+ return res;
+ }
+
+ /**
+ * Expand the Cuboid in the given direction by the given amount. Negative amounts will shrink the Cuboid in the
+ * given direction. Shrinking a cuboid's face past the opposite face is not an error and will return a valid
+ * Cuboid.
+ *
+ * @param dir the direction in which to expand
+ * @param amount the number of blocks by which to expand
+ *
+ * @return a new Cuboid expanded by the given direction and amount
+ */
+ public Cuboid expand(CuboidDirection dir, int amount) {
+ switch (dir) {
+ case NORTH:
+ return new Cuboid(worldName, x1 - amount, y1, z1, x2, y2, z2);
+ case SOUTH:
+ return new Cuboid(worldName, x1, y1, z1, x2 + amount, y2, z2);
+ case EASY:
+ return new Cuboid(worldName, x1, y1, z1 - amount, x2, y2, z2);
+ case WEST:
+ return new Cuboid(worldName, x1, y1, z1, x2, y2, z2 + amount);
+ case DOWN:
+ return new Cuboid(worldName, x1, y1 - amount, z1, x2, y2, z2);
+ case UP:
+ return new Cuboid(worldName, x1, y1, z1, x2, y2 + amount, z2);
+ default:
+ throw new IllegalArgumentException("invalid direction " + dir);
+ }
+ }
+
+ /**
+ * Shift the Cuboid in the given direction by the given amount.
+ *
+ * @param dir the direction in which to shift
+ * @param amount the number of blocks by which to shift
+ *
+ * @return a new Cuboid shifted by the given direction and amount
+ */
+ public Cuboid shift(CuboidDirection dir, int amount) {
+ return expand(dir, amount).expand(dir.opposite(), -amount);
+ }
+
+ /**
+ * Outset (grow) the Cuboid in the given direction by the given amount.
+ *
+ * @param dir the direction in which to outset (must be HORIZONTAL, VERTICAL, or BOTH)
+ * @param amount the number of blocks by which to outset
+ *
+ * @return a new Cuboid outset by the given direction and amount
+ */
+ public Cuboid outset(CuboidDirection dir, int amount) {
+ Cuboid c;
+ switch (dir) {
+ case HORIZONTAL:
+ c = expand(CuboidDirection.NORTH, amount).expand(CuboidDirection.SOUTH, amount)
+ .expand(CuboidDirection.EASY, amount)
+ .expand(CuboidDirection.WEST, amount);
+ break;
+ case VERTICAL:
+ c = expand(CuboidDirection.DOWN, amount).expand(CuboidDirection.UP, amount);
+ break;
+ case BOTH:
+ c = outset(CuboidDirection.HORIZONTAL, amount).outset(CuboidDirection.VERTICAL, amount);
+ break;
+ default:
+ throw new IllegalArgumentException("invalid direction " + dir);
+ }
+ return c;
+ }
+
+ /**
+ * Inset (shrink) the Cuboid in the given direction by the given amount. Equivalent to calling outset() with a
+ * negative amount.
+ *
+ * @param dir the direction in which to inset (must be HORIZONTAL, VERTICAL, or BOTH)
+ * @param amount the number of blocks by which to inset
+ *
+ * @return a new Cuboid inset by the given direction and amount
+ */
+ public Cuboid inset(CuboidDirection dir, int amount) {
+ return outset(dir, -amount);
+ }
+
+ /**
+ * Return true if the point at (x,y,z) is contained within this Cuboid.
+ *
+ * @param x the X coord
+ * @param y the Y coord
+ * @param z the Z coord
+ *
+ * @return true if the given point is within this Cuboid, false otherwise
+ */
+ public boolean contains(int x, int y, int z) {
+ return x >= x1 && x <= x2 && y >= y1 && y <= y2 && z >= z1 && z <= z2;
+ }
+
+ /**
+ * Return true if the point at (x,z) is contained within this Cuboid.
+ *
+ * @param x the X coord
+ * @param z the Z coord
+ *
+ * @return true if the given point is within this Cuboid, false otherwise
+ */
+ public boolean contains(int x, int z) {
+ return x >= x1 && x <= x2 && z >= z1 && z <= z2;
+ }
+
+ /**
+ * Check if the given Location is contained within this Cuboid.
+ *
+ * @param l the Location to check for
+ *
+ * @return true if the Location is within this Cuboid, false otherwise
+ */
+ public boolean contains(Location l) {
+ if (!worldName.equals(l.getWorld().getName())) {
+ return false;
+ }
+ return contains(l.getBlockX(), l.getBlockY(), l.getBlockZ());
+ }
+
+ /**
+ * Check if the given Block is contained within this Cuboid.
+ *
+ * @param b the Block to check for
+ *
+ * @return true if the Block is within this Cuboid, false otherwise
+ */
+ public boolean contains(Block b) {
+ return contains(b.getLocation());
+ }
+
+ /**
+ * Get the volume of this Cuboid.
+ *
+ * @return the Cuboid volume, in blocks
+ */
+ public int volume() {
+ return getSizeX() * getSizeY() * getSizeZ();
+ }
+
+ /**
+ * Get the Cuboid representing the face of this Cuboid. The resulting Cuboid will be one block thick in the axis
+ * perpendicular to the requested face.
+ *
+ * @param dir which face of the Cuboid to get
+ *
+ * @return the Cuboid representing this Cuboid's requested face
+ */
+ public Cuboid getFace(CuboidDirection dir) {
+ switch (dir) {
+ case DOWN:
+ return new Cuboid(worldName, x1, y1, z1, x2, y1, z2);
+ case UP:
+ return new Cuboid(worldName, x1, y2, z1, x2, y2, z2);
+ case NORTH:
+ return new Cuboid(worldName, x1, y1, z1, x1, y2, z2);
+ case SOUTH:
+ return new Cuboid(worldName, x2, y1, z1, x2, y2, z2);
+ case EASY:
+ return new Cuboid(worldName, x1, y1, z1, x2, y2, z1);
+ case WEST:
+ return new Cuboid(worldName, x1, y1, z2, x2, y2, z2);
+ default:
+ throw new IllegalArgumentException("Invalid direction " + dir);
+ }
+ }
+
+ /**
+ * Get the Cuboid big enough to hold both this Cuboid and the given one.
+ *
+ * @return a new Cuboid large enough to hold this Cuboid and the given Cuboid
+ */
+ public Cuboid getBoundingCuboid(Cuboid other) {
+ if (other == null) {
+ return this;
+ }
+
+ int xMin = Math.min(getLowerX(), other.getLowerX());
+ int yMin = Math.min(getLowerY(), other.getLowerY());
+ int zMin = Math.min(getLowerZ(), other.getLowerZ());
+ int xMax = Math.max(getUpperX(), other.getUpperX());
+ int yMax = Math.max(getUpperY(), other.getUpperY());
+ int zMax = Math.max(getUpperZ(), other.getUpperZ());
+
+ return new Cuboid(worldName, xMin, yMin, zMin, xMax, yMax, zMax);
+ }
+
+ /**
+ * Get a block relative to the lower NE point of the Cuboid.
+ *
+ * @param x the X coord
+ * @param y the Y coord
+ * @param z the Z coord
+ *
+ * @return the block at the given position
+ */
+ public Block getRelativeBlock(int x, int y, int z) {
+ return getWorld().getBlockAt(x1 + x, y1 + y, z1 + z);
+ }
+
+ /**
+ * Get a block relative to the lower NE point of the Cuboid in the given World. This version of getRelativeBlock()
+ * should be used if being called many times, to avoid excessive calls to getWorld().
+ *
+ * @param w the World
+ * @param x the X coord
+ * @param y the Y coord
+ * @param z the Z coord
+ *
+ * @return the block at the given position
+ */
+ public Block getRelativeBlock(World w, int x, int y, int z) {
+ return w.getBlockAt(x1 + x, y1 + y, z1 + z);
+ }
+
+ /**
+ * Get a list of the chunks which are fully or partially contained in this cuboid.
+ *
+ * @return a list of Chunk objects
+ */
+ public List getChunks() {
+ List chunks = new ArrayList();
+
+ World w = getWorld();
+
+ // These operators get the lower bound of the chunk, by complementing 0xf (15) into 16
+ // and using an OR gate on the integer coordinate
+
+ int x1 = getLowerX() & ~0xf;
+ int x2 = getUpperX() & ~0xf;
+ int z1 = getLowerZ() & ~0xf;
+ int z2 = getUpperZ() & ~0xf;
+
+ for (int x = x1; x <= x2; x += 16) {
+ for (int z = z1; z <= z2; z += 16) {
+ chunks.add(w.getChunkAt(x >> 4, z >> 4));
+ }
+ }
+
+ return chunks;
+ }
+
+ /**
+ * @return horizontal walls of the cuboid
+ */
+ public Cuboid[] getWalls() {
+
+ return new Cuboid[]{
+ getFace(CuboidDirection.NORTH),
+ getFace(CuboidDirection.SOUTH),
+ getFace(CuboidDirection.WEST),
+ getFace(CuboidDirection.EASY)
+ };
+ }
+
+ /**
+ * @return read-only location iterator
+ */
+ public Iterator iterator() {
+ return new LocationCuboidIterator(getWorld(), x1, y1, z1, x2, y2, z2);
+ }
+
+ @Override
+ public String toString() {
+ return "Cuboid: " + worldName + "," + x1 + "," + y1 + "," + z1 + "=>" + x2 + "," + y2 + "," + z2;
+ }
+
+ public class LocationCuboidIterator implements Iterator {
+
+ private World w;
+ private int baseX, baseY, baseZ;
+ private int x, y, z;
+ private int sizeX, sizeY, sizeZ;
+
+ public LocationCuboidIterator(World w, int x1, int y1, int z1, int x2, int y2, int z2) {
+ this.w = w;
+ baseX = x1;
+ baseY = y1;
+ baseZ = z1;
+ sizeX = Math.abs(x2 - x1) + 1;
+ sizeY = Math.abs(y2 - y1) + 1;
+ sizeZ = Math.abs(z2 - z1) + 1;
+ x = y = z = 0;
+ }
+
+ public boolean hasNext() {
+ return x < sizeX && y < sizeY && z < sizeZ;
+ }
+
+ public Location next() {
+ Location b = new Location(w, baseX + x, baseY + y, baseZ + z);
+ if (++x >= sizeX) {
+ x = 0;
+ if (++y >= sizeY) {
+ y = 0;
+ ++z;
+ }
+ }
+ return b;
+ }
+
+ public void remove() {
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/cuboid/CuboidDirection.java b/plugin/src/main/java/me/joeleoli/praxi/cuboid/CuboidDirection.java
new file mode 100644
index 0000000..71135fa
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/cuboid/CuboidDirection.java
@@ -0,0 +1,43 @@
+package me.joeleoli.praxi.cuboid;
+
+/**
+ * Represents directions that can be applied to certain faces and actions of a Cuboid
+ */
+public enum CuboidDirection {
+
+ NORTH,
+ EASY,
+ SOUTH,
+ WEST,
+ UP,
+ DOWN,
+ HORIZONTAL,
+ VERTICAL,
+ BOTH,
+ UNKNOWN;
+
+ public CuboidDirection opposite() {
+ switch (this) {
+ case NORTH:
+ return SOUTH;
+ case EASY:
+ return WEST;
+ case SOUTH:
+ return NORTH;
+ case WEST:
+ return EASY;
+ case HORIZONTAL:
+ return VERTICAL;
+ case VERTICAL:
+ return HORIZONTAL;
+ case UP:
+ return DOWN;
+ case DOWN:
+ return UP;
+ case BOTH:
+ return BOTH;
+ default:
+ return UNKNOWN;
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugin/src/main/java/me/joeleoli/praxi/duel/DuelProcedure.java b/plugin/src/main/java/me/joeleoli/praxi/duel/DuelProcedure.java
new file mode 100644
index 0000000..71e4bad
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/duel/DuelProcedure.java
@@ -0,0 +1,57 @@
+package me.joeleoli.praxi.duel;
+
+import java.text.MessageFormat;
+import lombok.Data;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.apache.commons.lang3.StringUtils;
+import org.bukkit.entity.Player;
+
+@Data
+public class DuelProcedure {
+
+ private static final HoverEvent ACCEPT_HOVER = new HoverEvent(
+ HoverEvent.Action.SHOW_TEXT,
+ new ChatComponentBuilder(Style.YELLOW + "Click to accept this duel invite.").create()
+ );
+
+ private Player sender;
+ private Player target;
+ private Ladder ladder;
+ private Arena arena;
+
+ public void send() {
+ if (!this.sender.isOnline() || !this.target.isOnline()) {
+ return;
+ }
+
+ final DuelRequest request = new DuelRequest(this.sender.getUniqueId());
+
+ request.setLadder(this.ladder);
+ request.setArena(this.arena);
+
+ final PraxiPlayer senderData = PraxiPlayer.getByUuid(this.sender.getUniqueId());
+
+ senderData.setDuelProcedure(null);
+ senderData.getSentDuelRequests().put(this.target.getUniqueId(), request);
+
+ final ClickEvent click = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/duel accept " + this.sender.getName());
+ final String ladderContext = StringUtils.startsWithIgnoreCase(this.ladder.getName(), "u") ? "an " : "a ";
+
+ this.sender.sendMessage(Style.translate(new MessageFormat("&eYou sent a duel request to &d{0} &eon arena &d{1}&e.")
+ .format(new Object[]{ this.target.getName(), this.arena.getName() })));
+ this.target.sendMessage(Style.translate(
+ new MessageFormat("&d{0} &esent you {1} &d{2} &eduel request on arena &d{3}&e.").format(new Object[]{
+ this.sender.getName(), ladderContext, this.ladder.getName(), this.arena.getName()
+ })));
+ this.target.sendMessage(new ChatComponentBuilder("")
+ .parse("&6Click here or type &b/duel accept " + this.sender.getName() + " &6to accept the invite.")
+ .attachToEachPart(click).attachToEachPart(ACCEPT_HOVER).create());
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/duel/DuelRequest.java b/plugin/src/main/java/me/joeleoli/praxi/duel/DuelRequest.java
new file mode 100644
index 0000000..09011ee
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/duel/DuelRequest.java
@@ -0,0 +1,24 @@
+package me.joeleoli.praxi.duel;
+
+import java.util.UUID;
+import lombok.Data;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+
+@Data
+public class DuelRequest {
+
+ private UUID sender;
+ private Ladder ladder;
+ private Arena arena;
+ private long timestamp = System.currentTimeMillis();
+
+ public DuelRequest(UUID uuid) {
+ this.sender = uuid;
+ }
+
+ public boolean isExpired() {
+ return System.currentTimeMillis() - this.timestamp >= 30_000;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectArenaMenu.java b/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectArenaMenu.java
new file mode 100644
index 0000000..49a1028
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectArenaMenu.java
@@ -0,0 +1,94 @@
+package me.joeleoli.praxi.duel.gui;
+
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.arena.ArenaType;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class DuelSelectArenaMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.BLUE + Style.BOLD + "Select an arena";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ Map buttons = new HashMap<>();
+
+ for (Arena arena : Arena.getArenas()) {
+ if (!arena.isSetup()) {
+ continue;
+ }
+
+ if (!arena.getLadders().contains(praxiPlayer.getDuelProcedure().getLadder().getName())) {
+ continue;
+ }
+
+ if (praxiPlayer.getDuelProcedure().getLadder().isBuild() && arena.getType() == ArenaType.SHARED) {
+ continue;
+ }
+
+ if (praxiPlayer.getDuelProcedure().getLadder().isBuild() && arena.getType() != ArenaType.STANDALONE) {
+ continue;
+ }
+
+ if (praxiPlayer.getDuelProcedure().getLadder().isBuild() && arena.isActive()) {
+ continue;
+ }
+
+ buttons.put(buttons.size(), new SelectArenaButton(arena));
+ }
+
+ return buttons;
+ }
+
+ @Override
+ public void onClose(Player player) {
+ if (!this.isClosedByMenu()) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setDuelProcedure(null);
+ }
+ }
+
+ @AllArgsConstructor
+ private class SelectArenaButton extends Button {
+
+ private Arena arena;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.PAPER).name(Style.GREEN + Style.BOLD + this.arena.getName()).build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ // Update and request the procedure
+ praxiPlayer.getDuelProcedure().setArena(this.arena);
+ praxiPlayer.getDuelProcedure().send();
+
+ // Set closed by menu
+ Menu.currentlyOpenedMenus.get(player.getName()).setClosedByMenu(true);
+
+ // Force close inventory
+ player.closeInventory();
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectLadderMenu.java b/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectLadderMenu.java
new file mode 100644
index 0000000..40d342a
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/duel/gui/DuelSelectLadderMenu.java
@@ -0,0 +1,82 @@
+package me.joeleoli.praxi.duel.gui;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class DuelSelectLadderMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.GOLD + Style.BOLD + "Select a ladder";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ Map buttons = new HashMap<>();
+
+ for (Ladder ladder : Ladder.getLadders()) {
+ if (ladder.isEnabled()) {
+ buttons.put(buttons.size(), new SelectLadderButton(ladder));
+ }
+ }
+
+ return buttons;
+ }
+
+ @Override
+ public void onClose(Player player) {
+ if (!this.isClosedByMenu()) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setDuelProcedure(null);
+ }
+ }
+
+ @AllArgsConstructor
+ private class SelectLadderButton extends Button {
+
+ private Ladder ladder;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(this.ladder.getDisplayIcon())
+ .name(Style.PINK + Style.BOLD + this.ladder.getName())
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click here to select " + Style.PINK + Style.BOLD +
+ this.ladder.getName() + Style.YELLOW + "."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ // Update duel procedure
+ praxiPlayer.getDuelProcedure().setLadder(this.ladder);
+
+ // Set closed by menu
+ Menu.currentlyOpenedMenus.get(player.getName()).setClosedByMenu(true);
+
+ // Force close inventory
+ player.closeInventory();
+
+ // Open arena selection menu
+ new DuelSelectArenaMenu().openMenu(player);
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/elo/EloUtil.java b/plugin/src/main/java/me/joeleoli/praxi/elo/EloUtil.java
new file mode 100644
index 0000000..b955957
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/elo/EloUtil.java
@@ -0,0 +1,55 @@
+package me.joeleoli.praxi.elo;
+
+public class EloUtil {
+
+ private static final KFactor[] K_FACTORS = {
+ new KFactor(0, 1000, 25),
+ new KFactor(1001, 1400, 20),
+ new KFactor(1401, 1800, 15),
+ new KFactor(1801, 2200, 10)
+ };
+
+ private static final int DEFAULT_K_FACTOR = 25;
+ private static final int WIN = 1;
+ private static final int LOSS = 0;
+
+ public static int getNewRating(int rating, int opponentRating, boolean won) {
+ if (won) {
+ return EloUtil.getNewRating(rating, opponentRating, EloUtil.WIN);
+ } else {
+ return EloUtil.getNewRating(rating, opponentRating, EloUtil.LOSS);
+ }
+ }
+
+ public static int getNewRating(int rating, int opponentRating, int score) {
+ double kFactor = EloUtil.getKFactor(rating);
+ double expectedScore = EloUtil.getExpectedScore(rating, opponentRating);
+ int newRating = EloUtil.calculateNewRating(rating, score, expectedScore, kFactor);
+
+ if (score == 1) {
+ if (newRating == rating) {
+ newRating++;
+ }
+ }
+ return newRating;
+ }
+
+ private static int calculateNewRating(int oldRating, int score, double expectedScore, double kFactor) {
+ return oldRating + (int) (kFactor * (score - expectedScore));
+ }
+
+ private static double getKFactor(int rating) {
+ for (int i = 0; i < EloUtil.K_FACTORS.length; i++) {
+ if (rating >= EloUtil.K_FACTORS[i].getStartIndex() && rating <= EloUtil.K_FACTORS[i].getEndIndex()) {
+ return EloUtil.K_FACTORS[i].getValue();
+ }
+ }
+
+ return EloUtil.DEFAULT_K_FACTOR;
+ }
+
+ private static double getExpectedScore(int rating, int opponentRating) {
+ return 1 / (1 + Math.pow(10, ((double) (opponentRating - rating) / 400)));
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/elo/KFactor.java b/plugin/src/main/java/me/joeleoli/praxi/elo/KFactor.java
new file mode 100644
index 0000000..00dc09e
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/elo/KFactor.java
@@ -0,0 +1,14 @@
+package me.joeleoli.praxi.elo;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class KFactor {
+
+ private final int startIndex;
+ private final int endIndex;
+ private final double value;
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/Event.java b/plugin/src/main/java/me/joeleoli/praxi/events/Event.java
new file mode 100644
index 0000000..3e54202
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/Event.java
@@ -0,0 +1,253 @@
+package me.joeleoli.praxi.events;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.player.PlayerInfo;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.events.task.EventStartTask;
+import me.joeleoli.praxi.player.PlayerState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+@Getter
+public abstract class Event {
+
+ protected static final String EVENT_PREFIX = Style.GOLD + Style.BOLD + "[Event] " + Style.RESET;
+ private static final HoverEvent HOVER_EVENT = new HoverEvent(
+ HoverEvent.Action.SHOW_TEXT,
+ new ChatComponentBuilder("").parse(Style.YELLOW + "Click to join the Sumo event.").create()
+ );
+ private static final ClickEvent CLICK_EVENT = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/event join");
+
+ private String name;
+ @Setter
+ private EventState state = EventState.WAITING;
+ private EventTask eventTask;
+ private PlayerInfo host;
+ private Map eventPlayers = new HashMap<>();
+ private int maxPlayers;
+ @Setter
+ private Cooldown cooldown;
+
+ public Event(String name, PlayerInfo host, int maxPlayers) {
+ this.name = name;
+ this.host = host;
+ this.maxPlayers = maxPlayers;
+ }
+
+ public void setEventTask(EventTask task) {
+ if (this.eventTask != null) {
+ this.eventTask.cancel();
+ }
+
+ this.eventTask = task;
+
+ if (this.eventTask != null) {
+ this.eventTask.runTaskTimer(Praxi.getInstance(), 0L, 20L);
+ }
+ }
+
+ public boolean isWaiting() {
+ return this.state == EventState.WAITING;
+ }
+
+ public boolean isFighting() {
+ return this.state == EventState.ROUND_FIGHTING;
+ }
+
+ public EventPlayer getEventPlayer(UUID uuid) {
+ return this.eventPlayers.get(uuid);
+ }
+
+ public List getPlayers() {
+ List players = new ArrayList<>();
+
+ for (EventPlayer eventPlayer : this.eventPlayers.values()) {
+ final Player player = eventPlayer.toPlayer();
+
+ if (player != null) {
+ players.add(player);
+ }
+ }
+
+ return players;
+ }
+
+ public int getRemainingPlayers() {
+ int remaining = 0;
+
+ for (EventPlayer eventPlayer : this.eventPlayers.values()) {
+ if (eventPlayer.getState() == EventPlayerState.WAITING) {
+ remaining++;
+ }
+ }
+
+ return remaining;
+ }
+
+ public void handleStart() {
+ this.setEventTask(new EventStartTask(this));
+ }
+
+ public void handleJoin(Player player) {
+ this.eventPlayers.put(player.getUniqueId(), new EventPlayer(player));
+ this.broadcastMessage(Style.PINK + player.getName() + Style.YELLOW + " joined the event " + Style.PINK + "(" +
+ this.getRemainingPlayers() + "/" + this.getMaxPlayers() + ")");
+ this.onJoin(player);
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setEvent(this);
+ praxiPlayer.setState(PlayerState.IN_EVENT);
+ praxiPlayer.loadHotbar();
+
+ player.teleport(Praxi.getInstance().getEventManager().getSumoSpectator());
+ }
+
+ public void handleDeath(Player player) {
+ final EventPlayer loser = this.getEventPlayer(player.getUniqueId());
+
+ loser.setState(EventPlayerState.ELIMINATED);
+
+ this.onDeath(player);
+ }
+
+ public void handleLeave(Player player) {
+ if (this.isFighting(player.getUniqueId())) {
+ this.handleDeath(player);
+ }
+
+ this.eventPlayers.remove(player.getUniqueId());
+ this.onLeave(player);
+
+ this.getPlayers().forEach(otherPlayer -> {
+ player.hidePlayer(otherPlayer);
+ otherPlayer.hidePlayer(player);
+ });
+
+ if (this.state == EventState.WAITING) {
+ this.broadcastMessage(Style.PINK + player.getName() + Style.YELLOW + " left the event " + Style.PINK +
+ "(" + this.getRemainingPlayers() + "/" + this.getMaxPlayers() + ")");
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.setEvent(null);
+ praxiPlayer.loadHotbar();
+
+ PlayerUtil.spawn(player);
+ }
+
+ public void end() {
+ // Remove active event and set cooldown
+ Praxi.getInstance().getEventManager().setActiveEvent(null);
+ Praxi.getInstance().getEventManager().setEventCooldown(new Cooldown(60_000L * 3));
+
+ // Cancel any active task
+ this.setEventTask(null);
+
+ final Player winner = this.getWinner();
+ final List players = this.getPlayers();
+
+ if (winner == null) {
+ PlayerUtil.messageAll(EVENT_PREFIX + Style.YELLOW + "The event has been canceled.");
+ } else {
+ PlayerUtil.messageAll(EVENT_PREFIX + Style.PINK + winner.getName() + Style.YELLOW + " has won the event!");
+ }
+
+ for (EventPlayer eventPlayer : this.eventPlayers.values()) {
+ final Player player = eventPlayer.toPlayer();
+
+ if (player != null) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.setEvent(null);
+ praxiPlayer.loadHotbar();
+
+ PlayerUtil.spawn(player);
+ }
+ }
+
+ players.forEach(player -> players.forEach(otherPlayer -> {
+ player.hidePlayer(otherPlayer);
+ otherPlayer.hidePlayer(player);
+ }));
+ }
+
+ public boolean canEnd() {
+ int remaining = 0;
+
+ for (EventPlayer eventPlayer : this.eventPlayers.values()) {
+ if (eventPlayer.getState() == EventPlayerState.WAITING) {
+ remaining++;
+ }
+ }
+
+ return remaining == 1;
+ }
+
+ public Player getWinner() {
+ for (EventPlayer eventPlayer : this.eventPlayers.values()) {
+ if (eventPlayer.getState() != EventPlayerState.ELIMINATED) {
+ return eventPlayer.toPlayer();
+ }
+ }
+
+ return null;
+ }
+
+ public void announce() {
+ BaseComponent[] components = new ChatComponentBuilder("")
+ .parse(EVENT_PREFIX + Style.PINK + this.getHost().getName() + Style.YELLOW + " is hosting a " +
+ Style.PINK + this.getName() + " Event " + Style.GRAY + "[Click to join]")
+ .attachToEachPart(HOVER_EVENT)
+ .attachToEachPart(CLICK_EVENT)
+ .create();
+
+ for (Player player : Bukkit.getOnlinePlayers()) {
+ player.sendMessage(components);
+ }
+ }
+
+ public void broadcastMessage(String message) {
+ for (Player player : this.getPlayers()) {
+ player.sendMessage(EVENT_PREFIX + message);
+ }
+ }
+
+ public abstract boolean isSumo();
+
+ public abstract boolean isCorners();
+
+ public abstract void onJoin(Player player);
+
+ public abstract void onLeave(Player player);
+
+ public abstract void onRound();
+
+ public abstract void onDeath(Player player);
+
+ public abstract String getRoundDuration();
+
+ public abstract EventPlayer getRoundPlayerA();
+
+ public abstract EventPlayer getRoundPlayerB();
+
+ public abstract boolean isFighting(UUID uuid);
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/EventManager.java b/plugin/src/main/java/me/joeleoli/praxi/events/EventManager.java
new file mode 100644
index 0000000..e61cfb0
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/EventManager.java
@@ -0,0 +1,77 @@
+package me.joeleoli.praxi.events;
+
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.config.ConfigCursor;
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.util.LocationUtil;
+import me.joeleoli.praxi.Praxi;
+import org.bukkit.Location;
+
+@Getter
+@Setter
+public class EventManager {
+
+ private Event activeEvent;
+ private Cooldown eventCooldown = new Cooldown(0);
+ private Location sumoSpectator, sumoSpawn1, sumoSpawn2;
+ private String sumoKbProfile;
+
+ public void setActiveEvent(Event event) {
+ if (this.activeEvent != null) {
+ this.activeEvent.setEventTask(null);
+ }
+
+ if (event == null) {
+ this.activeEvent = null;
+ return;
+ }
+
+ this.activeEvent = event;
+ this.activeEvent.handleStart();
+ this.activeEvent.handleJoin(event.getHost().toPlayer());
+ }
+
+ public void load() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getMainConfig(), "event");
+
+ if (cursor.exists("sumo.spectator")) {
+ this.sumoSpectator = LocationUtil.deserialize(cursor.getString("sumo.spectator"));
+ }
+
+ if (cursor.exists("sumo.spawn1")) {
+ this.sumoSpawn1 = LocationUtil.deserialize(cursor.getString("sumo.spawn1"));
+ }
+
+ if (cursor.exists("sumo.spawn2")) {
+ this.sumoSpawn2 = LocationUtil.deserialize(cursor.getString("sumo.spawn2"));
+ }
+
+ if (cursor.exists("sumo.kb-profile")) {
+ this.sumoKbProfile = cursor.getString("sumo.kb-profile");
+ }
+ }
+
+ public void save() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getMainConfig(), "event");
+
+ if (this.sumoSpectator != null) {
+ cursor.set("sumo.spectator", LocationUtil.serialize(this.sumoSpectator));
+ }
+
+ if (this.sumoSpawn1 != null) {
+ cursor.set("sumo.spawn1", LocationUtil.serialize(this.sumoSpawn1));
+ }
+
+ if (this.sumoSpawn2 != null) {
+ cursor.set("sumo.spawn2", LocationUtil.serialize(this.sumoSpawn2));
+ }
+
+ if (this.sumoKbProfile != null) {
+ cursor.set("sumo.kb-profile", this.sumoKbProfile);
+ }
+
+ cursor.save();
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayer.java b/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayer.java
new file mode 100644
index 0000000..a7cf719
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayer.java
@@ -0,0 +1,23 @@
+package me.joeleoli.praxi.events;
+
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.player.PlayerInfo;
+import org.bukkit.entity.Player;
+
+@Getter
+@Setter
+public class EventPlayer extends PlayerInfo {
+
+ private EventPlayerState state = EventPlayerState.WAITING;
+ private int roundWins = 0;
+
+ public EventPlayer(Player player) {
+ super(player.getUniqueId(), player.getName());
+ }
+
+ public void incrementRoundWins() {
+ this.roundWins++;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayerState.java b/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayerState.java
new file mode 100644
index 0000000..1670f51
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/EventPlayerState.java
@@ -0,0 +1,15 @@
+package me.joeleoli.praxi.events;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum EventPlayerState {
+
+ WAITING("Waiting"),
+ ELIMINATED("Eliminated");
+
+ private String readable;
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/EventState.java b/plugin/src/main/java/me/joeleoli/praxi/events/EventState.java
new file mode 100644
index 0000000..9a1aef7
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/EventState.java
@@ -0,0 +1,10 @@
+package me.joeleoli.praxi.events;
+
+public enum EventState {
+
+ WAITING,
+ ROUND_STARTING,
+ ROUND_FIGHTING,
+ ROUND_ENDING,
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/EventTask.java b/plugin/src/main/java/me/joeleoli/praxi/events/EventTask.java
new file mode 100644
index 0000000..170fe78
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/EventTask.java
@@ -0,0 +1,37 @@
+package me.joeleoli.praxi.events;
+
+import lombok.Getter;
+import me.joeleoli.praxi.Praxi;
+import org.bukkit.scheduler.BukkitRunnable;
+
+@Getter
+public abstract class EventTask extends BukkitRunnable {
+
+ private int ticks;
+ private Event event;
+ private EventState eventState;
+
+ public EventTask(Event event, EventState eventState) {
+ this.event = event;
+ this.eventState = eventState;
+ }
+
+ @Override
+ public void run() {
+ if (Praxi.getInstance().getEventManager().getActiveEvent() == null || !Praxi.getInstance().getEventManager().getActiveEvent().equals(this.event) || this.event.getState() != this.eventState) {
+ this.cancel();
+ return;
+ }
+
+ this.onRun();
+
+ this.ticks++;
+ }
+
+ public int getSeconds() {
+ return 3 - this.ticks;
+ }
+
+ public abstract void onRun();
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/impl/SumoEvent.java b/plugin/src/main/java/me/joeleoli/praxi/events/impl/SumoEvent.java
new file mode 100644
index 0000000..6687c3b
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/impl/SumoEvent.java
@@ -0,0 +1,165 @@
+package me.joeleoli.praxi.events.impl;
+
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.player.PlayerInfo;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TimeUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventPlayer;
+import me.joeleoli.praxi.events.EventPlayerState;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.events.task.EventRoundEndTask;
+import me.joeleoli.praxi.events.task.EventRoundStartTask;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+
+@Getter
+public class SumoEvent extends Event {
+
+ private EventPlayer roundPlayerA;
+ private EventPlayer roundPlayerB;
+ @Setter
+ private long roundStart;
+
+ public SumoEvent(Player player) {
+ super("Sumo", new PlayerInfo(player), 100);
+ }
+
+ @Override
+ public boolean isSumo() {
+ return true;
+ }
+
+ @Override
+ public boolean isCorners() {
+ return false;
+ }
+
+ @Override
+ public void onJoin(Player player) {
+ this.getPlayers().forEach(otherPlayer -> {
+ player.showPlayer(otherPlayer);
+ otherPlayer.showPlayer(player);
+ });
+ }
+
+ @Override
+ public void onLeave(Player player) {
+ player.setKnockbackProfile(null);
+ }
+
+ @Override
+ public void onRound() {
+ this.setState(EventState.ROUND_STARTING);
+
+ if (this.roundPlayerA != null) {
+ final Player player = this.roundPlayerA.toPlayer();
+
+ if (player != null) {
+ player.teleport(Praxi.getInstance().getEventManager().getSumoSpectator());
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInEvent()) {
+ praxiPlayer.loadHotbar();
+ }
+ }
+
+ this.roundPlayerA = null;
+ }
+
+ if (this.roundPlayerB != null) {
+ final Player player = this.roundPlayerB.toPlayer();
+
+ if (player != null) {
+ player.teleport(Praxi.getInstance().getEventManager().getSumoSpectator());
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInEvent()) {
+ praxiPlayer.loadHotbar();
+ }
+ }
+
+ this.roundPlayerB = null;
+ }
+
+ this.roundPlayerA = this.findRoundPlayer();
+ this.roundPlayerB = this.findRoundPlayer();
+
+ final Player playerA = this.roundPlayerA.toPlayer();
+ final Player playerB = this.roundPlayerB.toPlayer();
+
+ PlayerUtil.reset(playerA);
+ PlayerUtil.reset(playerB);
+
+ PlayerUtil.denyMovement(playerA);
+ PlayerUtil.denyMovement(playerB);
+
+ playerA.teleport(Praxi.getInstance().getEventManager().getSumoSpawn1());
+ playerB.teleport(Praxi.getInstance().getEventManager().getSumoSpawn2());
+
+ this.setEventTask(new EventRoundStartTask(this));
+ }
+
+ @Override
+ public void onDeath(Player player) {
+ final EventPlayer winner = this.roundPlayerA.getUuid().equals(player.getUniqueId()) ? this.roundPlayerB : this.roundPlayerA;
+
+ winner.setState(EventPlayerState.WAITING);
+ winner.incrementRoundWins();
+
+ this.broadcastMessage(Style.PINK + player.getName() + Style.YELLOW + " was eliminated by " + Style.PINK + winner.getName() + Style.YELLOW + "!");
+ this.setState(EventState.ROUND_ENDING);
+ this.setEventTask(new EventRoundEndTask(this));
+ }
+
+ @Override
+ public String getRoundDuration() {
+ if (this.getState() == EventState.ROUND_STARTING) {
+ return "00:00";
+ } else if (this.getState() == EventState.ROUND_FIGHTING) {
+ return TimeUtil.millisToTimer(System.currentTimeMillis() - this.roundStart);
+ } else {
+ return "Ending";
+ }
+ }
+
+ @Override
+ public boolean isFighting(UUID uuid) {
+ return (this.roundPlayerA != null && this.roundPlayerA.getUuid().equals(uuid)) || (this.roundPlayerB != null && this.roundPlayerB.getUuid().equals(uuid));
+ }
+
+ private EventPlayer findRoundPlayer() {
+ EventPlayer eventPlayer = null;
+
+ for (EventPlayer check : this.getEventPlayers().values()) {
+ if (!this.isFighting(check.getUuid()) && check.getState() == EventPlayerState.WAITING) {
+ if (eventPlayer == null) {
+ eventPlayer = check;
+ continue;
+ }
+
+ if (check.getRoundWins() == 0) {
+ eventPlayer = check;
+ continue;
+ }
+
+ if (check.getRoundWins() <= eventPlayer.getRoundWins()) {
+ eventPlayer = check;
+ }
+ }
+ }
+
+ if (eventPlayer == null) {
+ throw new RuntimeException("Could not find a new round player");
+ }
+
+ return eventPlayer;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundEndTask.java b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundEndTask.java
new file mode 100644
index 0000000..2f314c2
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundEndTask.java
@@ -0,0 +1,24 @@
+package me.joeleoli.praxi.events.task;
+
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.events.EventTask;
+
+public class EventRoundEndTask extends EventTask {
+
+ public EventRoundEndTask(Event event) {
+ super(event, EventState.ROUND_ENDING);
+ }
+
+ @Override
+ public void onRun() {
+ if (this.getTicks() >= 3) {
+ if (this.getEvent().canEnd()) {
+ this.getEvent().end();
+ } else {
+ this.getEvent().onRound();
+ }
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundStartTask.java b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundStartTask.java
new file mode 100644
index 0000000..3b9920b
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventRoundStartTask.java
@@ -0,0 +1,39 @@
+package me.joeleoli.praxi.events.task;
+
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.events.EventTask;
+import me.joeleoli.praxi.events.impl.SumoEvent;
+import org.bukkit.entity.Player;
+
+public class EventRoundStartTask extends EventTask {
+
+ public EventRoundStartTask(Event event) {
+ super(event, EventState.ROUND_STARTING);
+ }
+
+ @Override
+ public void onRun() {
+ if (this.getTicks() >= 3) {
+ this.getEvent().setEventTask(null);
+ this.getEvent().setState(EventState.ROUND_FIGHTING);
+
+ final Player playerA = this.getEvent().getRoundPlayerA().toPlayer();
+ final Player playerB = this.getEvent().getRoundPlayerB().toPlayer();
+
+ PlayerUtil.allowMovement(playerA);
+ PlayerUtil.allowMovement(playerB);
+
+ ((SumoEvent) this.getEvent()).setRoundStart(System.currentTimeMillis());
+ } else {
+ final int seconds = this.getSeconds();
+
+ this.getEvent().broadcastMessage(
+ Style.YELLOW + "The round will start in " + Style.PINK + (seconds) + " second" +
+ (seconds == 1 ? "" : "s") + Style.YELLOW + "...");
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/events/task/EventStartTask.java b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventStartTask.java
new file mode 100644
index 0000000..a8531a7
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/events/task/EventStartTask.java
@@ -0,0 +1,45 @@
+package me.joeleoli.praxi.events.task;
+
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.events.EventTask;
+
+public class EventStartTask extends EventTask {
+
+ public EventStartTask(Event event) {
+ super(event, EventState.WAITING);
+ }
+
+ @Override
+ public void onRun() {
+ if (this.getTicks() >= 600) {
+ this.getEvent().end();
+ return;
+ }
+
+ if (this.getEvent().getPlayers().size() <= 1 && this.getEvent().getCooldown() != null) {
+ this.getEvent().setCooldown(null);
+ this.getEvent().broadcastMessage(Style.YELLOW + "There are not enough players for the event to start.");
+ }
+
+ if (this.getEvent().getPlayers().size() == this.getEvent().getMaxPlayers() || (this.getTicks() >= 30 && this.getEvent().getPlayers().size() >= 2)) {
+ if (this.getEvent().getCooldown() == null) {
+ this.getEvent().setCooldown(new Cooldown(11_000));
+ this.getEvent().broadcastMessage(Style.YELLOW + "The event will start in " + Style.PINK + "10 seconds" + Style.YELLOW + "...");
+ } else {
+ if (this.getEvent().getCooldown().hasExpired()) {
+ this.getEvent().setState(EventState.ROUND_STARTING);
+ this.getEvent().onRound();
+ this.getEvent().setEventTask(new EventRoundStartTask(this.getEvent()));
+ }
+ }
+ }
+
+ if (this.getTicks() % 10 == 0) {
+ this.getEvent().announce();
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/handler/PlayerMovementHandler.java b/plugin/src/main/java/me/joeleoli/praxi/handler/PlayerMovementHandler.java
new file mode 100644
index 0000000..03b2f34
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/handler/PlayerMovementHandler.java
@@ -0,0 +1,67 @@
+package me.joeleoli.praxi.handler;
+
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.events.EventState;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.ragespigot.handler.MovementHandler;
+import net.minecraft.server.v1_8_R3.PacketPlayInFlying;
+import org.bukkit.GameMode;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+
+public class PlayerMovementHandler implements MovementHandler {
+
+ @Override
+ public void handleUpdateLocation(Player player, Location from, Location to, PacketPlayInFlying packetPlayInFlying) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInLobby() || praxiPlayer.isInQueue()) {
+ if (player.getGameMode() != GameMode.CREATIVE) {
+ if (to.getX() >= 200 || to.getX() <= -200 || to.getZ() >= 200 || to.getZ() <= -200) {
+ PlayerUtil.spawn(player);
+ }
+ }
+ } else if (praxiPlayer.isInMatch()) {
+ if (praxiPlayer.getMatch().getLadder().isSumo() || praxiPlayer.getMatch().getLadder().isSpleef()) {
+ final Match match = praxiPlayer.getMatch();
+
+ if (match.isFighting()) {
+ if (player.getLocation().getBlock().getType() == Material.WATER ||
+ player.getLocation().getBlock().getType() == Material.STATIONARY_WATER) {
+ Player killer = player.getKiller();
+
+ if (killer == null) {
+ if (match.isSoloMatch()) {
+ killer = praxiPlayer.getMatch().getOpponentPlayer(player);
+ }
+ }
+
+ match.handleDeath(player, killer, false);
+ }
+ }
+ }
+ } else if (praxiPlayer.isInEvent()) {
+ final Event event = praxiPlayer.getEvent();
+
+ if (event.isSumo()) {
+ if (event.getState() == EventState.ROUND_FIGHTING) {
+ if (event.isFighting(player.getUniqueId())) {
+ if (player.getLocation().getBlock().getType() == Material.WATER ||
+ player.getLocation().getBlock().getType() == Material.STATIONARY_WATER) {
+ event.handleDeath(player);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void handleUpdateRotation(Player player, Location from, Location to, PacketPlayInFlying packetPlayInFlying) {
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/Kit.java b/plugin/src/main/java/me/joeleoli/praxi/kit/Kit.java
new file mode 100644
index 0000000..5cd942a
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/Kit.java
@@ -0,0 +1,27 @@
+package me.joeleoli.praxi.kit;
+
+import lombok.Data;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+@Data
+public class Kit {
+
+ public static final ItemStack DEFAULT_KIT = new ItemBuilder(Material.BOOK).name(Style.GOLD + "Default Kit").build();
+
+ private ItemStack[] armor;
+ private ItemStack[] contents;
+
+ public Kit() {
+ this.armor = new ItemStack[4];
+ this.contents = new ItemStack[36];
+ }
+
+ public Kit(ItemStack[] armor, ItemStack[] contents) {
+ this.armor = armor;
+ this.contents = contents;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/NamedKit.java b/plugin/src/main/java/me/joeleoli/praxi/kit/NamedKit.java
new file mode 100644
index 0000000..6930c48
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/NamedKit.java
@@ -0,0 +1,12 @@
+package me.joeleoli.praxi.kit;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@AllArgsConstructor
+@Data
+public class NamedKit extends Kit {
+
+ private String name;
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitEditorMenu.java b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitEditorMenu.java
new file mode 100644
index 0000000..a1f3731
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitEditorMenu.java
@@ -0,0 +1,280 @@
+package me.joeleoli.praxi.kit.gui;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.menu.buttons.DisplayButton;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.ItemUtil;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.praxi.kit.NamedKit;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+
+public class KitEditorMenu extends Menu {
+
+ private static final int[] ITEM_POSITIONS = new int[]{
+ 20, 21, 22, 23, 24, 25, 26, 29, 30, 31, 32, 33, 34, 35, 38, 39, 40, 41, 42, 43, 44, 47, 48, 49, 50, 51, 52,
+ 53
+ };
+ private static final int[] BORDER_POSITIONS = new int[]{ 1, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 28, 37, 46 };
+ private static final Button BORDER_BUTTON = Button.placeholder(Material.COAL_BLOCK, (byte) 0, " ");
+
+ public KitEditorMenu() {
+ this.setUpdateAfterClick(false);
+ }
+
+ @Override
+ public String getTitle(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ return Style.GOLD + "Editing " + Style.AQUA + praxiPlayer.getKitEditor().getSelectedKit().getName();
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ NamedKit kit = praxiPlayer.getKitEditor().getSelectedKit();
+ Map buttons = new HashMap<>();
+
+ for (int border : BORDER_POSITIONS) {
+ buttons.put(border, BORDER_BUTTON);
+ }
+
+ buttons.put(0, new CurrentKitButton());
+ buttons.put(2, new SaveButton());
+ buttons.put(6, new LoadDefaultKitButton());
+ buttons.put(7, new ClearInventoryButton());
+ buttons.put(8, new CancelButton());
+ buttons.put(18, new ArmorDisplayButton(kit.getArmor()[3]));
+ buttons.put(27, new ArmorDisplayButton(kit.getArmor()[2]));
+ buttons.put(36, new ArmorDisplayButton(kit.getArmor()[1]));
+ buttons.put(45, new ArmorDisplayButton(kit.getArmor()[0]));
+
+ List items = praxiPlayer.getKitEditor().getSelectedLadder().getKitEditorItems();
+
+ for (int i = 20; i < (praxiPlayer.getKitEditor().getSelectedLadder().getKitEditorItems().size() + 20); i++) {
+ buttons.put(ITEM_POSITIONS[i - 20], new InfiniteItemButton(items.get(i - 20)));
+ }
+
+ return buttons;
+ }
+
+ @Override
+ public void onOpen(Player player) {
+ if (!this.isClosedByMenu()) {
+ PlayerUtil.reset(player);
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ praxiPlayer.getKitEditor().setActive(true);
+
+ if (praxiPlayer.getKitEditor().getSelectedKit() != null) {
+ player.getInventory().setContents(praxiPlayer.getKitEditor().getSelectedKit().getContents());
+ }
+
+ player.updateInventory();
+ }
+ }
+
+ @Override
+ public void onClose(Player player) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ praxiPlayer.getKitEditor().setActive(false);
+
+ if (!praxiPlayer.isInMatch()) {
+ TaskUtil.runLater(praxiPlayer::loadHotbar, 1L);
+ }
+ }
+
+ @AllArgsConstructor
+ private class ArmorDisplayButton extends Button {
+
+ private ItemStack itemStack;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ if (this.itemStack == null || this.itemStack.getType() == Material.AIR) {
+ return new ItemStack(Material.AIR);
+ }
+
+ return new ItemBuilder(this.itemStack.clone())
+ .name(Style.AQUA + ItemUtil.getName(this.itemStack))
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "This is automatically equipped."
+ ))
+ .build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class CurrentKitButton extends Button {
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ return new ItemBuilder(Material.NAME_TAG)
+ .name(Style.GREEN + Style.BOLD + "Editing: " + Style.AQUA +
+ praxiPlayer.getKitEditor().getSelectedKit().getName())
+ .build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class ClearInventoryButton extends Button {
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.STAINED_CLAY)
+ .durability(7)
+ .name(Style.YELLOW + Style.BOLD + "Clear Inventory")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "This will clear your inventory",
+ Style.YELLOW + "so you can start over."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ Button.playNeutral(player);
+ player.getInventory().setContents(new ItemStack[36]);
+ player.updateInventory();
+ }
+
+ @Override
+ public boolean shouldUpdate(Player player, int i, ClickType clickType) {
+ return true;
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class LoadDefaultKitButton extends Button {
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.STAINED_CLAY)
+ .durability(7)
+ .name(Style.YELLOW + Style.BOLD + "Load default kit")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click this to load the default kit",
+ Style.YELLOW + "into the kit editing menu."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ Button.playNeutral(player);
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ player.getInventory()
+ .setContents(praxiPlayer.getKitEditor().getSelectedLadder().getDefaultKit().getContents());
+ player.updateInventory();
+ }
+
+ @Override
+ public boolean shouldUpdate(Player player, int i, ClickType clickType) {
+ return true;
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class SaveButton extends Button {
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.STAINED_CLAY)
+ .durability(5)
+ .name(Style.GREEN + Style.BOLD + "Save")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click this to save your kit."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ Button.playNeutral(player);
+ player.closeInventory();
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getKitEditor().getSelectedKit() != null) {
+ praxiPlayer.getKitEditor().getSelectedKit().setContents(player.getInventory().getContents());
+ }
+
+ praxiPlayer.loadHotbar();
+
+ new KitManagementMenu(praxiPlayer.getKitEditor().getSelectedLadder()).openMenu(player);
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class CancelButton extends Button {
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.STAINED_CLAY)
+ .durability(14)
+ .name(Style.RED + Style.BOLD + "Cancel")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click this to abort editing your kit,",
+ Style.YELLOW + "and return to the kit menu."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ Button.playNeutral(player);
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getKitEditor().getSelectedLadder() != null) {
+ new KitManagementMenu(praxiPlayer.getKitEditor().getSelectedLadder()).openMenu(player);
+ }
+ }
+
+ }
+
+ private class InfiniteItemButton extends DisplayButton {
+
+ InfiniteItemButton(ItemStack itemStack) {
+ super(itemStack, false);
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ final Inventory inventory = player.getOpenInventory().getTopInventory();
+ final ItemStack itemStack = inventory.getItem(i);
+
+ inventory.setItem(i, itemStack);
+
+ player.setItemOnCursor(itemStack);
+ player.updateInventory();
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitManagementMenu.java b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitManagementMenu.java
new file mode 100644
index 0000000..ba09789
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/KitManagementMenu.java
@@ -0,0 +1,239 @@
+package me.joeleoli.praxi.kit.gui;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.kit.NamedKit;
+import me.joeleoli.praxi.kit.gui.button.BackButton;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class KitManagementMenu extends Menu {
+
+ private static final Button PLACEHOLDER = Button.placeholder(Material.STAINED_GLASS_PANE, (byte) 7, " ");
+
+ private Ladder ladder;
+
+ public KitManagementMenu(Ladder ladder) {
+ this.ladder = ladder;
+
+ this.setPlaceholder(true);
+ this.setUpdateAfterClick(false);
+ }
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.GOLD + "Viewing " + this.ladder.getName() + " kits";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final Map buttons = new HashMap<>();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ NamedKit[] kits = praxiPlayer.getKits(this.ladder);
+
+ if (kits == null) {
+ return buttons;
+ }
+
+ int startPos = -1;
+
+ for (int i = 0; i < 4; i++) {
+ NamedKit kit = kits[i];
+ startPos += 2;
+
+ buttons.put(startPos, kit == null ? new CreateKitButton(i) : new KitDisplayButton(kit));
+ buttons.put(startPos + 18, new LoadKitButton(i));
+ buttons.put(startPos + 27, kit == null ? PLACEHOLDER : new RenameKitButton(kit));
+ buttons.put(startPos + 36, kit == null ? PLACEHOLDER : new DeleteKitButton(kit));
+ }
+
+ buttons.put(36, new BackButton(new SelectLadderKitMenu()));
+
+ return buttons;
+ }
+
+ @Override
+ public void onClose(Player player) {
+ if (!this.isClosedByMenu()) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setState(praxiPlayer.getKitEditor().getPreviousState());
+ praxiPlayer.getKitEditor().setSelectedLadder(null);
+ }
+ }
+
+ @AllArgsConstructor
+ private class DeleteKitButton extends Button {
+
+ private NamedKit kit;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.STAINED_CLAY)
+ .name(Style.RED + Style.BOLD + "Delete")
+ .durability(14)
+ .lore(Arrays.asList(
+ "",
+ Style.RED + "Click to delete this kit.",
+ Style.RED + "You will " + Style.BOLD + "NOT" + Style.RED + " be able to",
+ Style.RED + "recover this kit."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.deleteKit(praxiPlayer.getKitEditor().getSelectedLadder(), this.kit);
+
+ new KitManagementMenu(praxiPlayer.getKitEditor().getSelectedLadder()).openMenu(player);
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class CreateKitButton extends Button {
+
+ private int index;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.IRON_SWORD)
+ .name(Style.GREEN + Style.BOLD + "Create Kit")
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final Ladder ladder = praxiPlayer.getKitEditor().getSelectedLadder();
+
+ // TODO: this shouldn't be null but sometimes it is?
+ if (ladder == null) {
+ player.closeInventory();
+ return;
+ }
+
+ final NamedKit kit = new NamedKit("Kit " + (this.index + 1));
+
+ if (ladder.getDefaultKit() != null) {
+ if (ladder.getDefaultKit().getArmor() != null) {
+ kit.setArmor(ladder.getDefaultKit().getArmor());
+ }
+
+ if (ladder.getDefaultKit().getContents() != null) {
+ kit.setContents(ladder.getDefaultKit().getContents());
+ }
+ }
+
+ praxiPlayer.replaceKit(ladder, this.index, kit);
+ praxiPlayer.getKitEditor().setSelectedKit(kit);
+
+ new KitEditorMenu().openMenu(player);
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class RenameKitButton extends Button {
+
+ private NamedKit kit;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.SIGN)
+ .name(Style.YELLOW + Style.BOLD + "Rename")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click to rename this kit."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ Menu.currentlyOpenedMenus.get(player.getName()).setClosedByMenu(true);
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.getKitEditor().setActive(true);
+ praxiPlayer.getKitEditor().setRename(true);
+ praxiPlayer.getKitEditor().setSelectedKit(this.kit);
+
+ player.closeInventory();
+ player.sendMessage(
+ Style.YELLOW + "Renaming " + Style.BOLD + this.kit.getName() + Style.YELLOW + "... " + Style.GREEN +
+ "Enter the new name now.");
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class LoadKitButton extends Button {
+
+ private int index;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.BOOK)
+ .name(Style.GREEN + Style.BOLD + "Load/Edit")
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click to edit this kit."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ // TODO: this shouldn't be null but sometimes it is?
+ if (praxiPlayer.getKitEditor().getSelectedLadder() == null) {
+ player.closeInventory();
+ return;
+ }
+
+ NamedKit kit = praxiPlayer.getKit(praxiPlayer.getKitEditor().getSelectedLadder(), this.index);
+
+ if (kit == null) {
+ kit = new NamedKit("Kit " + (this.index + 1));
+ kit.setArmor(praxiPlayer.getKitEditor().getSelectedLadder().getDefaultKit().getArmor());
+ kit.setContents(praxiPlayer.getKitEditor().getSelectedLadder().getDefaultKit().getContents());
+
+ praxiPlayer.replaceKit(praxiPlayer.getKitEditor().getSelectedLadder(), this.index, kit);
+ }
+
+ praxiPlayer.getKitEditor().setSelectedKit(kit);
+
+ new KitEditorMenu().openMenu(player);
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class KitDisplayButton extends Button {
+
+ private NamedKit kit;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.BOOK)
+ .name(Style.GREEN + Style.BOLD + this.kit.getName())
+ .build();
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/gui/SelectLadderKitMenu.java b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/SelectLadderKitMenu.java
new file mode 100644
index 0000000..0071da5
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/SelectLadderKitMenu.java
@@ -0,0 +1,67 @@
+package me.joeleoli.praxi.kit.gui;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class SelectLadderKitMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.GOLD + Style.BOLD + "Select a ladder";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ Map buttons = new HashMap<>();
+
+ Ladder.getLadders().forEach(ladder -> {
+ if (ladder.isEnabled()) {
+ buttons.put(buttons.size(), new LadderKitDisplayButton(ladder));
+ }
+ });
+
+ return buttons;
+ }
+
+ @AllArgsConstructor
+ private class LadderKitDisplayButton extends Button {
+
+ private Ladder ladder;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(this.ladder.getDisplayIcon())
+ .name(Style.PINK + Style.BOLD + this.ladder.getName())
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click to select " + Style.PINK + Style.BOLD + this.ladder.getName() +
+ Style.YELLOW + "."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ player.closeInventory();
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.getKitEditor().setSelectedLadder(this.ladder);
+ praxiPlayer.getKitEditor().setPreviousState(praxiPlayer.getState());
+
+ new KitManagementMenu(this.ladder).openMenu(player);
+ }
+
+ }
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/kit/gui/button/BackButton.java b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/button/BackButton.java
new file mode 100644
index 0000000..b6811b2
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/kit/gui/button/BackButton.java
@@ -0,0 +1,33 @@
+package me.joeleoli.praxi.kit.gui.button;
+
+import java.util.Arrays;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+
+@AllArgsConstructor
+public class BackButton extends Button {
+
+ private Menu back;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.REDSTONE).name(Style.RED + Style.BOLD + "Back").lore(Arrays
+ .asList(Style.RED + "Click here to return to", Style.RED + "the previous menu.")).build();
+ }
+
+ @Override
+ public void clicked(Player player, int i, ClickType clickType, int hb) {
+ Button.playNeutral(player);
+
+ this.back.openMenu(player);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/ladder/Ladder.java b/plugin/src/main/java/me/joeleoli/praxi/ladder/Ladder.java
new file mode 100644
index 0000000..a2cc1dd
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/ladder/Ladder.java
@@ -0,0 +1,86 @@
+package me.joeleoli.praxi.ladder;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Data;
+import lombok.Getter;
+import me.joeleoli.nucleus.config.ConfigCursor;
+import me.joeleoli.nucleus.util.InventoryUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.kit.Kit;
+import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+@Data
+public class Ladder {
+
+ @Getter
+ private static List ladders = new ArrayList<>();
+
+ private String name;
+ private String displayName;
+ private ItemStack displayIcon;
+ private Kit defaultKit = new Kit();
+ private List kitEditorItems = new ArrayList<>();
+ private boolean enabled, build, sumo, parkour, spleef, regeneration, allowPotionFill;
+ private int hitDelay = 20;
+ private String kbProfile;
+
+ public Ladder(String name) {
+ this.name = name;
+ this.displayName = ChatColor.AQUA + this.name;
+ this.displayIcon = new ItemStack(Material.DIAMOND_SWORD);
+
+ ladders.add(this);
+ }
+
+ public static Ladder getByName(String name) {
+ for (Ladder ladder : ladders) {
+ if (ladder.getName().equalsIgnoreCase(name)) {
+ return ladder;
+ }
+ }
+
+ return null;
+ }
+
+ public ItemStack getDisplayIcon() {
+ return this.displayIcon.clone();
+ }
+
+ public void save() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getLadderConfig(), "ladders." + this.name);
+
+ cursor.set("display-name", this.displayName);
+ cursor.set("display-icon.material", this.displayIcon.getType().name());
+ cursor.set("display-icon.durability", this.displayIcon.getDurability());
+ cursor.set("display-icon.amount", this.displayIcon.getAmount());
+ cursor.set("enabled", this.enabled);
+ cursor.set("build", this.build);
+ cursor.set("sumo", this.sumo);
+ cursor.set("spleef", this.spleef);
+ cursor.set("parkour", this.parkour);
+ cursor.set("regeneration", this.regeneration);
+ cursor.set("hit-delay", this.hitDelay);
+
+ if (this.displayIcon.hasItemMeta()) {
+ final ItemMeta itemMeta = this.displayIcon.getItemMeta();
+
+ if (itemMeta.hasDisplayName()) {
+ cursor.set("display-icon.name", itemMeta.getDisplayName());
+ }
+
+ if (itemMeta.hasLore()) {
+ cursor.set("display-icon.lore", itemMeta.getLore());
+ }
+ }
+
+ cursor.set("default-kit.armor", InventoryUtil.serializeInventory(this.defaultKit.getArmor()));
+ cursor.set("default-kit.contents", InventoryUtil.serializeInventory(this.defaultKit.getContents()));
+
+ cursor.save();
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/ArenaListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/ArenaListener.java
new file mode 100644
index 0000000..f959c33
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/ArenaListener.java
@@ -0,0 +1,103 @@
+package me.joeleoli.praxi.listener;
+
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.arena.ArenaType;
+import me.joeleoli.praxi.arena.selection.Selection;
+import me.joeleoli.praxi.match.Match;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Event;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.Action;
+import org.bukkit.event.block.BlockFromToEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.inventory.ItemStack;
+
+public class ArenaListener implements Listener {
+
+ @EventHandler
+ public void onPlayerInteract(PlayerInteractEvent event) {
+ if (!(event.getAction() == Action.LEFT_CLICK_BLOCK || event.getAction() == Action.RIGHT_CLICK_BLOCK)) {
+ return;
+ }
+
+ Player player = event.getPlayer();
+ ItemStack item = event.getItem();
+
+ Block clicked = event.getClickedBlock();
+ int location = 0;
+
+ if (item == null || !item.equals(Selection.SELECTION_WAND)) {
+ return;
+ }
+
+ Selection selection = Selection.createOrGetSelection(player);
+
+ if (event.getAction() == Action.RIGHT_CLICK_BLOCK) {
+ selection.setPoint2(clicked.getLocation());
+ location = 2;
+ } else if (event.getAction() == Action.LEFT_CLICK_BLOCK) {
+ selection.setPoint1(clicked.getLocation());
+ location = 1;
+ }
+
+ event.setCancelled(true);
+ event.setUseItemInHand(Event.Result.DENY);
+ event.setUseInteractedBlock(Event.Result.DENY);
+
+ String message = Style.AQUA + (location == 1 ? "First" : "Second") +
+ " location " + Style.YELLOW + "(" + Style.GREEN +
+ clicked.getX() + Style.YELLOW + ", " + Style.GREEN +
+ clicked.getY() + Style.YELLOW + ", " + Style.GREEN +
+ clicked.getZ() + Style.YELLOW + ")" + Style.AQUA + " has been set!";
+
+ if (selection.isFullObject()) {
+ message += Style.RED + " (" + Style.YELLOW + selection.getCuboid().volume() + Style.AQUA + " blocks" +
+ Style.RED + ")";
+ }
+
+ player.sendMessage(message);
+ }
+
+ @EventHandler
+ public void onBlockFromTo(BlockFromToEvent event) {
+ final int x = event.getBlock().getX();
+ final int y = event.getBlock().getY();
+ final int z = event.getBlock().getZ();
+
+ Arena foundArena = null;
+
+ for (Arena arena : Arena.getArenas()) {
+ if (!(arena.getType() == ArenaType.STANDALONE || arena.getType() == ArenaType.DUPLICATE)) {
+ continue;
+ }
+
+ if (!arena.isActive()) {
+ continue;
+ }
+
+ if (x >= arena.getX1() && x <= arena.getX2() && y >= arena.getY1() && y <= arena.getY2() &&
+ z >= arena.getZ1() && z <= arena.getZ2()) {
+ foundArena = arena;
+ break;
+ }
+ }
+
+ if (foundArena == null) {
+ return;
+ }
+
+ for (Match match : Match.getMatches()) {
+ if (match.getArena().equals(foundArena)) {
+ if (match.isFighting()) {
+ match.getPlacedBlocks().add(event.getToBlock().getLocation());
+ }
+
+ break;
+ }
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/EntityListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/EntityListener.java
new file mode 100644
index 0000000..4b6fc94
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/EntityListener.java
@@ -0,0 +1,178 @@
+package me.joeleoli.praxi.listener;
+
+import me.joeleoli.nucleus.Nucleus;
+import me.joeleoli.nucleus.util.BukkitUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchTeam;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Arrow;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.EntityDamageByEntityEvent;
+import org.bukkit.event.entity.EntityDamageEvent;
+import org.bukkit.event.entity.EntityRegainHealthEvent;
+import org.bukkit.event.entity.FoodLevelChangeEvent;
+
+public class EntityListener implements Listener {
+
+ @EventHandler
+ public void onEntityRegainHealth(EntityRegainHealthEvent event) {
+ if (event.getEntity() instanceof Player) {
+ if (event.getRegainReason() == EntityRegainHealthEvent.RegainReason.SATIATED) {
+ final Player player = (Player) event.getEntity();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ if (!praxiPlayer.getMatch().getLadder().isRegeneration()) {
+ event.setCancelled(true);
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onEntityDamage(EntityDamageEvent event) {
+ if (event.getEntity() instanceof Player) {
+ final Player player = (Player) event.getEntity();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ if (event.getCause() == EntityDamageEvent.DamageCause.VOID) {
+ praxiPlayer.getMatch().handleDeath(player, null, false);
+ return;
+ }
+
+ if (!praxiPlayer.getMatch().isFighting()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (praxiPlayer.getMatch().isTeamMatch()) {
+ if (!praxiPlayer.getMatch().getMatchPlayer(player).isAlive()) {
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ if (praxiPlayer.getMatch().getLadder().isSumo() || praxiPlayer.getMatch().getLadder().isSpleef()) {
+ event.setDamage(0);
+ player.setHealth(20.0);
+ player.updateInventory();
+ }
+ } else if (praxiPlayer.isInEvent()) {
+ if (event.getCause() == EntityDamageEvent.DamageCause.VOID) {
+ praxiPlayer.getEvent().handleDeath(player);
+ return;
+ }
+
+ if (praxiPlayer.getEvent().isSumo()) {
+ if (!praxiPlayer.getEvent().isFighting() || !praxiPlayer.getEvent().isFighting(player.getUniqueId())) {
+ event.setCancelled(true);
+ return;
+ }
+
+ event.setDamage(0);
+ player.setHealth(20.0);
+ player.updateInventory();
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
+ public void onEntityDamageByEntity(EntityDamageByEntityEvent event) {
+ final Player attacker = BukkitUtil.getDamager(event);
+
+ if (attacker != null && event.getEntity() instanceof Player) {
+ final Player damaged = (Player) event.getEntity();
+ final PraxiPlayer damagedData = PraxiPlayer.getByUuid(damaged.getUniqueId());
+ final PraxiPlayer attackerData = PraxiPlayer.getByUuid(attacker.getUniqueId());
+
+ if (attackerData.isSpectating() || damagedData.isSpectating()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (damagedData.isInMatch() && attackerData.isInMatch()) {
+ final Match match = attackerData.getMatch();
+
+
+ if (!damagedData.getMatch().getMatchId().equals(attackerData.getMatch().getMatchId())) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (!match.getMatchPlayer(damaged).isAlive() || !match.getMatchPlayer(attacker).isAlive()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (match.isSoloMatch()) {
+ attackerData.getMatch().getMatchPlayer(attacker).handleHit();
+ damagedData.getMatch().getMatchPlayer(damaged).resetCombo();
+
+ if (event.getDamager() instanceof Arrow) {
+ double health = Math.ceil(damaged.getHealth() - event.getFinalDamage()) / 2.0D;
+
+ attacker.sendMessage(Style.formatArrowHitMessage(damaged.getName(), health));
+ }
+ } else if (match.isTeamMatch()) {
+ final MatchTeam attackerTeam = match.getTeam(attacker);
+ final MatchTeam damagedTeam = match.getTeam(damaged);
+
+ if (attackerTeam == null || damagedTeam == null) {
+ event.setCancelled(true);
+ } else {
+ if (attackerTeam.equals(damagedTeam)) {
+ event.setCancelled(true);
+ } else {
+ attackerData.getMatch().getMatchPlayer(attacker).handleHit();
+ damagedData.getMatch().getMatchPlayer(damaged).resetCombo();
+
+ if (event.getDamager() instanceof Arrow) {
+ double health = Math.ceil(damaged.getHealth() - event.getFinalDamage()) / 2.0D;
+
+ attacker.sendMessage(Style.formatArrowHitMessage(damaged.getName(), health));
+ }
+ }
+ }
+ }
+ } else if (damagedData.isInEvent() && attackerData.isInEvent()) {
+ final Event praxiEvent = damagedData.getEvent();
+
+ if (!praxiEvent.isFighting() || !praxiEvent.isFighting(damaged.getUniqueId()) || !praxiEvent.isFighting(attacker.getUniqueId())) {
+ event.setCancelled(true);
+ }
+ }
+ }
+ }
+
+ @EventHandler
+ public void onFoodLevelChange(FoodLevelChangeEvent event) {
+ if (event.getEntity() instanceof Player) {
+ final Player player = (Player) event.getEntity();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInMatch() && praxiPlayer.getMatch().isFighting()) {
+ if (event.getFoodLevel() >= 20) {
+ event.setFoodLevel(20);
+ player.setSaturation(20);
+ } else {
+ event.setCancelled(Nucleus.RANDOM.nextInt(100) > 25);
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/PlayerListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/PlayerListener.java
new file mode 100644
index 0000000..15ab3af
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/PlayerListener.java
@@ -0,0 +1,624 @@
+package me.joeleoli.praxi.listener;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import me.joeleoli.nucleus.command.CommandHandler;
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.player.gui.ViewPlayerMenu;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.nucleus.util.TimeUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.kit.Kit;
+import me.joeleoli.praxi.kit.NamedKit;
+import me.joeleoli.praxi.kit.gui.KitManagementMenu;
+import me.joeleoli.praxi.kit.gui.SelectLadderKitMenu;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.party.gui.OtherPartiesMenu;
+import me.joeleoli.praxi.party.gui.PartyEventSelectEventMenu;
+import me.joeleoli.praxi.player.PlayerHotbar;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.queue.Queue;
+import me.joeleoli.praxi.queue.gui.QueueJoinMenu;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.GameMode;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Item;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.event.entity.PlayerDeathEvent;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.player.AsyncPlayerChatEvent;
+import org.bukkit.event.player.AsyncPlayerPreLoginEvent;
+import org.bukkit.event.player.PlayerBucketEmptyEvent;
+import org.bukkit.event.player.PlayerDropItemEvent;
+import org.bukkit.event.player.PlayerInteractEntityEvent;
+import org.bukkit.event.player.PlayerInteractEvent;
+import org.bukkit.event.player.PlayerItemConsumeEvent;
+import org.bukkit.event.player.PlayerItemDamageEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerKickEvent;
+import org.bukkit.event.player.PlayerPickupItemEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.player.PlayerRespawnEvent;
+import org.bukkit.inventory.CraftingInventory;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.potion.PotionEffect;
+import org.bukkit.potion.PotionEffectType;
+
+public class PlayerListener implements Listener {
+
+ @EventHandler
+ public void onPlayerItemConsume(PlayerItemConsumeEvent event) {
+ if (event.getItem().getType() == Material.GOLDEN_APPLE) {
+ if (event.getItem().hasItemMeta() &&
+ event.getItem().getItemMeta().getDisplayName().contains("Golden Head")) {
+ final Player player = event.getPlayer();
+
+ player.addPotionEffect(new PotionEffect(PotionEffectType.REGENERATION, 200, 1));
+ player.addPotionEffect(new PotionEffect(PotionEffectType.ABSORPTION, 2400, 0));
+ player.setFoodLevel(Math.min(player.getFoodLevel() + 6, 20));
+ }
+ }
+ }
+
+ @EventHandler
+ public void onPlayerInteractEntity(PlayerInteractEntityEvent event) {
+ final Player player = event.getPlayer();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isSpectating() && event.getRightClicked() instanceof Player && player.getItemInHand() != null) {
+ final Player target = (Player) event.getRightClicked();
+
+ if (PlayerHotbar.fromItemStack(player.getItemInHand()) == PlayerHotbar.HotbarItem.VIEW_INVENTORY) {
+ new ViewPlayerMenu(target).openMenu(player);
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
+ public void onAsyncPlayerChat(AsyncPlayerChatEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (event.getMessage().startsWith("@") || event.getMessage().startsWith("!")) {
+ if (praxiPlayer.getParty() != null) {
+ event.setCancelled(true);
+ praxiPlayer.getParty().broadcast(
+ Style.GOLD + "[Party]" + Style.RESET + " " + event.getPlayer().getDisplayName() + Style.RESET +
+ ": " + Style.strip(event.getMessage().substring(1)));
+ return;
+ }
+ }
+
+ if (praxiPlayer.getKitEditor().isRenaming()) {
+ event.setCancelled(true);
+
+ if (event.getMessage().length() > 16) {
+ event.getPlayer().sendMessage(Style.RED + "A kit name cannot be more than 16 characters long.");
+ return;
+ }
+
+ if (!praxiPlayer.isInMatch()) {
+ new KitManagementMenu(praxiPlayer.getKitEditor().getSelectedLadder()).openMenu(event.getPlayer());
+ }
+
+ praxiPlayer.getKitEditor().getSelectedKit().setName(event.getMessage());
+ praxiPlayer.getKitEditor().setActive(false);
+ praxiPlayer.getKitEditor().setRename(false);
+ praxiPlayer.getKitEditor().setSelectedKit(null);
+ }
+ }
+
+ @EventHandler
+ public void onAsyncPlayerPreLogin(AsyncPlayerPreLoginEvent event) {
+ PraxiPlayer praxiPlayer = new PraxiPlayer(event.getUniqueId(), null);
+
+ praxiPlayer.setName(event.getName());
+ praxiPlayer.load();
+
+ if (!praxiPlayer.isLoaded()) {
+ event.setLoginResult(AsyncPlayerPreLoginEvent.Result.KICK_OTHER);
+ event.setKickMessage(ChatColor.RED + "Failed to load your profile. Try again later.");
+ return;
+ }
+
+ PraxiPlayer.getPlayers().put(event.getUniqueId(), praxiPlayer);
+ }
+
+ @EventHandler
+ public void onPlayerJoin(PlayerJoinEvent event) {
+ event.setJoinMessage(null);
+
+ event.getPlayer().sendMessage(new String[]{
+ Style.getBorderLine(),
+ "",
+ Style.center(Style.YELLOW + "Welcome to " + Style.PINK + Style.BOLD + "MineXD Practice" +
+ Style.YELLOW + "!"),
+ "",
+ Style.center(Style.YELLOW + "Follow our twitter " + Style.PINK + "@MineXD" + Style.YELLOW +
+ " for updates and giveaways."),
+ "",
+ Style.getBorderLine()
+ });
+
+ PlayerUtil.spawn(event.getPlayer());
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+ praxiPlayer.loadHotbar();
+
+ Bukkit.getOnlinePlayers().forEach(player -> {
+ player.hidePlayer(event.getPlayer());
+ event.getPlayer().hidePlayer(player);
+ });
+ }
+
+ @EventHandler
+ public void onPlayerKick(PlayerKickEvent event) {
+ if (event.getReason() != null) {
+ if (event.getReason().contains("Flying is not enabled")) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.HIGHEST)
+ public void onPlayerQuit(PlayerQuitEvent event) {
+ event.setQuitMessage(null);
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getPlayers().remove(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer != null) {
+ if (praxiPlayer.getParty() != null) {
+ if (praxiPlayer.getParty().isLeader(event.getPlayer().getUniqueId())) {
+ praxiPlayer.getParty().disband();
+ } else {
+ praxiPlayer.getParty().leave(event.getPlayer(), false);
+ }
+ }
+
+ if (praxiPlayer.getRematchData() != null) {
+ final Player target =
+ Praxi.getInstance().getServer().getPlayer(praxiPlayer.getRematchData().getTarget());
+
+ if (target != null && target.isOnline()) {
+ PraxiPlayer.getByUuid(target.getUniqueId()).refreshHotbar();
+ }
+ }
+
+ TaskUtil.runAsync(praxiPlayer::save);
+
+ if (praxiPlayer.isInMatch()) {
+ praxiPlayer.getMatch().handleDeath(event.getPlayer(), null, true);
+ } else if (praxiPlayer.isInQueue()) {
+ Queue queue = Queue.getByUuid(praxiPlayer.getQueuePlayer().getQueueUuid());
+
+ if (queue == null) {
+ return;
+ }
+
+ queue.removePlayer(praxiPlayer.getQueuePlayer());
+ } else if (praxiPlayer.isInEvent()) {
+ praxiPlayer.getEvent().handleLeave(event.getPlayer());
+ }
+ }
+ }
+
+ @EventHandler(priority = EventPriority.LOW)
+ public void onPlayerInteract(PlayerInteractEvent event) {
+ if (event.getItem() != null && event.getAction().name().contains("RIGHT")) {
+ final Player player = event.getPlayer();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ if (event.getItem().hasItemMeta() && event.getItem().getItemMeta().hasDisplayName()) {
+ if (event.getItem().equals(Kit.DEFAULT_KIT)) {
+ event.setCancelled(true);
+
+ final Kit kit = praxiPlayer.getMatch().getLadder().getDefaultKit();
+
+ player.getInventory().setArmorContents(kit.getArmor());
+ player.getInventory().setContents(kit.getContents());
+ player.updateInventory();
+ player.sendMessage(
+ Style.YELLOW + "You have been given the" + Style.AQUA + " Default " + Style.YELLOW +
+ "kit.");
+ return;
+ }
+ }
+
+ if (event.getItem().hasItemMeta() && event.getItem().getItemMeta().hasDisplayName()) {
+ final String displayName = ChatColor.stripColor(event.getItem().getItemMeta().getDisplayName());
+
+ if (displayName.startsWith("Kit: ")) {
+ final String kitName = displayName.replace("Kit: ", "");
+
+ for (NamedKit kit : praxiPlayer.getKits(praxiPlayer.getMatch().getLadder())) {
+ if (kit != null) {
+ if (ChatColor.stripColor(kit.getName()).equals(kitName)) {
+ event.setCancelled(true);
+
+ player.getInventory().setArmorContents(kit.getArmor());
+ player.getInventory().setContents(kit.getContents());
+ player.updateInventory();
+ player.sendMessage(
+ Style.YELLOW + "You have been given the " + Style.AQUA + kit.getName() +
+ Style.YELLOW + " kit.");
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ if (event.getItem().getType() == Material.ENDER_PEARL ||
+ (event.getItem().getType() == Material.POTION && event.getItem().getDurability() >= 16_000)) {
+ if (praxiPlayer.isInMatch() && praxiPlayer.getMatch().isStarting()) {
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ if (event.getItem().getType() == Material.ENDER_PEARL && event.getClickedBlock() == null) {
+ if (!praxiPlayer.isInMatch() || (praxiPlayer.isInMatch() && !praxiPlayer.getMatch().isFighting())) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (praxiPlayer.getMatch().isStarting()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ if (!praxiPlayer.getEnderpearlCooldown().hasExpired()) {
+ final String time =
+ TimeUtil.millisToSeconds(praxiPlayer.getEnderpearlCooldown().getRemaining());
+ final String context = "second" + (time.equalsIgnoreCase("1.0") ? "s" : "");
+
+ event.setCancelled(true);
+ player.sendMessage(
+ Style.YELLOW + "You are on pearl cooldown for " + Style.PINK + time + " " + context +
+ Style.YELLOW + ".");
+ } else {
+ praxiPlayer.setEnderpearlCooldown(new Cooldown(16_000));
+ }
+ }
+ } else {
+ PlayerHotbar.HotbarItem hotbarItem = PlayerHotbar.fromItemStack(event.getItem());
+
+ if (hotbarItem == null) {
+ return;
+ }
+
+ event.setCancelled(true);
+
+ switch (hotbarItem) {
+ case QUEUE_JOIN_RANKED: {
+ if (praxiPlayer.isInLobby()) {
+ new QueueJoinMenu(true).openMenu(event.getPlayer());
+ }
+ }
+ break;
+ case QUEUE_JOIN_UNRANKED: {
+ if (praxiPlayer.isInLobby()) {
+ new QueueJoinMenu(false).openMenu(event.getPlayer());
+ }
+ }
+ break;
+ case QUEUE_LEAVE: {
+ if (praxiPlayer.isInQueue()) {
+ Queue queue = Queue.getByUuid(praxiPlayer.getQueuePlayer().getQueueUuid());
+
+ if (queue != null) {
+ queue.removePlayer(praxiPlayer.getQueuePlayer());
+ }
+ }
+ }
+ break;
+ case SPECTATE_STOP: {
+ CommandHandler.executeCommand(event.getPlayer(), "stopspectate");
+ }
+ break;
+ case PARTY_CREATE: {
+ CommandHandler.executeCommand(event.getPlayer(), "party create");
+ }
+ break;
+ case PARTY_DISBAND: {
+ CommandHandler.executeCommand(event.getPlayer(), "party disband");
+ }
+ break;
+ case PARTY_INFORMATION: {
+ CommandHandler.executeCommand(event.getPlayer(), "party info");
+ }
+ break;
+ case PARTY_LEAVE: {
+ CommandHandler.executeCommand(event.getPlayer(), "party leave");
+ }
+ break;
+ case PARTY_EVENTS: {
+ new PartyEventSelectEventMenu().openMenu(player);
+ }
+ break;
+ case OTHER_PARTIES: {
+ new OtherPartiesMenu().openMenu(event.getPlayer());
+ }
+ break;
+ case KIT_EDITOR: {
+ if (praxiPlayer.isInLobby() || praxiPlayer.isInQueue()) {
+ new SelectLadderKitMenu().openMenu(event.getPlayer());
+ }
+ }
+ break;
+ case SETTINGS: {
+ CommandHandler.executeCommand(event.getPlayer(), "settings");
+ }
+ break;
+ case REMATCH_REQUEST:
+ case REMATCH_ACCEPT: {
+ CommandHandler.executeCommand(event.getPlayer(), "rematch");
+ }
+ break;
+ case EVENT_JOIN: {
+ CommandHandler.executeCommand(event.getPlayer(), "event join");
+ }
+ break;
+ case EVENT_LEAVE: {
+ CommandHandler.executeCommand(event.getPlayer(), "event leave");
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ @EventHandler
+ public void onPlayerDeath(PlayerDeathEvent event) {
+ event.setDeathMessage(null);
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getEntity().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ final List- entities = new ArrayList<>();
+
+ event.getDrops().forEach(itemStack -> {
+ entities.add(event.getEntity().getLocation().getWorld()
+ .dropItemNaturally(event.getEntity().getLocation(), itemStack));
+ });
+ event.getDrops().clear();
+
+ praxiPlayer.getMatch().getEntities().addAll(entities);
+ praxiPlayer.getMatch().handleDeath(event.getEntity(), event.getEntity().getKiller(), false);
+ }
+ }
+
+ @EventHandler
+ public void onPlayerRespawn(PlayerRespawnEvent event) {
+ event.setRespawnLocation(event.getPlayer().getLocation());
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ praxiPlayer.getMatch().handleRespawn(event.getPlayer());
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onBlockPlace(BlockPlaceEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ final Match match = praxiPlayer.getMatch();
+
+ if (match.getLadder().isBuild() && praxiPlayer.getMatch().isFighting()) {
+ if (match.getLadder().isSpleef()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ final Arena arena = match.getArena();
+ final int x = (int) event.getBlockPlaced().getLocation().getX();
+ final int y = (int) event.getBlockPlaced().getLocation().getY();
+ final int z = (int) event.getBlockPlaced().getLocation().getZ();
+
+ if (y > arena.getMaxBuildHeight()) {
+ event.getPlayer().sendMessage(Style.RED + "You have reached the maximum build height.");
+ event.setCancelled(true);
+ return;
+ }
+
+ if (x >= arena.getX1() && x <= arena.getX2() && y >= arena.getY1() && y <= arena.getY2() &&
+ z >= arena.getZ1() && z <= arena.getZ2()) {
+ match.getPlacedBlocks().add(event.getBlock().getLocation());
+ } else {
+ event.setCancelled(true);
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ } else {
+ if (event.getPlayer().getGameMode() != GameMode.CREATIVE || !event.getPlayer().isOp()) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onBucketEmpty(PlayerBucketEmptyEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ final Match match = praxiPlayer.getMatch();
+
+ if (match.getLadder().isBuild() && praxiPlayer.getMatch().isFighting()) {
+ final Arena arena = match.getArena();
+ final Block block = event.getBlockClicked().getRelative(event.getBlockFace());
+ final int x = (int) block.getLocation().getX();
+ final int y = (int) block.getLocation().getY();
+ final int z = (int) block.getLocation().getZ();
+
+ if (y > arena.getMaxBuildHeight()) {
+ event.getPlayer().sendMessage(Style.RED + "You have reached the maximum build height.");
+ event.setCancelled(true);
+ return;
+ }
+
+ if (x >= arena.getX1() && x <= arena.getX2() && y >= arena.getY1() && y <= arena.getY2() &&
+ z >= arena.getZ1() && z <= arena.getZ2()) {
+ match.getPlacedBlocks().add(block.getLocation());
+ } else {
+ event.setCancelled(true);
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ } else {
+ if (event.getPlayer().getGameMode() != GameMode.CREATIVE || !event.getPlayer().isOp()) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onBlockBreak(BlockBreakEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ final Match match = praxiPlayer.getMatch();
+
+ if (match.getLadder().isBuild() && praxiPlayer.getMatch().isFighting()) {
+ if (match.getLadder().isSpleef()) {
+ if (event.getBlock().getType() == Material.SNOW_BLOCK ||
+ event.getBlock().getType() == Material.SNOW) {
+ match.getChangedBlocks().add(event.getBlock().getState());
+
+ event.getBlock().setType(Material.AIR);
+ event.getPlayer().getInventory().addItem(new ItemStack(Material.SNOW_BALL, 4));
+ event.getPlayer().updateInventory();
+ } else {
+ event.setCancelled(true);
+ }
+ } else if (!match.getPlacedBlocks().remove(event.getBlock().getLocation())) {
+ event.setCancelled(true);
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ } else {
+ if (event.getPlayer().getGameMode() != GameMode.CREATIVE || !event.getPlayer().isOp()) {
+ event.setCancelled(true);
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onPlayerPickupItem(PlayerPickupItemEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInMatch()) {
+ if (!praxiPlayer.getMatch().getMatchPlayer(event.getPlayer()).isAlive()) {
+ event.setCancelled(true);
+ return;
+ }
+
+ Iterator entityIterator = praxiPlayer.getMatch().getEntities().iterator();
+
+ while (entityIterator.hasNext()) {
+ Entity entity = entityIterator.next();
+
+ if (entity instanceof Item && entity.equals(event.getItem())) {
+ entityIterator.remove();
+ return;
+ }
+ }
+
+ event.setCancelled(true);
+ } else if (praxiPlayer.isSpectating()) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onPlayerDropItem(PlayerDropItemEvent event) {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (event.getItemDrop().getItemStack().getType() == Material.BOOK ||
+ event.getItemDrop().getItemStack().getType() == Material.ENCHANTED_BOOK) {
+ event.getItemDrop().remove();
+ return;
+ }
+
+ if (praxiPlayer.isInMatch()) {
+ if (praxiPlayer.getMatch() != null) {
+ if (event.getItemDrop().getItemStack().getType() == Material.GLASS_BOTTLE) {
+ event.getItemDrop().remove();
+ return;
+ }
+
+ praxiPlayer.getMatch().getEntities().add(event.getItemDrop());
+ }
+ } else {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler
+ public void onInventoryClick(InventoryClickEvent event) {
+ if (event.getWhoClicked() instanceof Player) {
+ final Player player = (Player) event.getWhoClicked();
+
+ if (event.getClickedInventory() != null && event.getClickedInventory() instanceof CraftingInventory) {
+ if (player.getGameMode() != GameMode.CREATIVE) {
+ event.setCancelled(true);
+ return;
+ }
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (!praxiPlayer.isInMatch() && player.getGameMode() == GameMode.SURVIVAL) {
+ final Inventory clicked = event.getClickedInventory();
+
+ if (praxiPlayer.getKitEditor().isActive()) {
+ if (clicked == null) {
+ event.setCancelled(true);
+ event.setCursor(null);
+ player.updateInventory();
+ } else if (clicked.equals(player.getOpenInventory().getTopInventory())) {
+ if (event.getCursor().getType() != Material.AIR &&
+ event.getCurrentItem().getType() == Material.AIR ||
+ event.getCursor().getType() != Material.AIR &&
+ event.getCurrentItem().getType() != Material.AIR) {
+ event.setCancelled(true);
+ event.setCursor(null);
+ player.updateInventory();
+ }
+ }
+ } else {
+ if (clicked != null && clicked.equals(player.getInventory())) {
+ event.setCancelled(true);
+ }
+ }
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onPlayerItemDamage(PlayerItemDamageEvent event) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(event.getPlayer().getUniqueId());
+
+ if (praxiPlayer.isInLobby()) {
+ event.setCancelled(true);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/ProjectileListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/ProjectileListener.java
new file mode 100644
index 0000000..b37e388
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/ProjectileListener.java
@@ -0,0 +1,58 @@
+package me.joeleoli.praxi.listener;
+
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Arrow;
+import org.bukkit.entity.Player;
+import org.bukkit.entity.ThrownPotion;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.entity.PotionSplashEvent;
+import org.bukkit.event.entity.ProjectileHitEvent;
+import org.bukkit.event.entity.ProjectileLaunchEvent;
+
+public class ProjectileListener implements Listener {
+
+ @EventHandler(ignoreCancelled = true)
+ public void onProjectileLaunch(ProjectileLaunchEvent event) {
+ if (event.getEntity() instanceof ThrownPotion) {
+ if (event.getEntity().getShooter() instanceof Player) {
+ final Player shooter = (Player) event.getEntity().getShooter();
+ final PraxiPlayer shooterData = PraxiPlayer.getByUuid(shooter.getUniqueId());
+
+ if (shooterData.isInMatch() && shooterData.getMatch().isFighting()) {
+ shooterData.getMatch().getMatchPlayer(shooter).incrementPotionsThrown();
+ }
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onProjectileHit(ProjectileHitEvent event) {
+ if (event.getEntity() instanceof Arrow) {
+ if (event.getEntity().getShooter() instanceof Player) {
+ final Player shooter = (Player) event.getEntity().getShooter();
+ final PraxiPlayer shooterData = PraxiPlayer.getByUuid(shooter.getUniqueId());
+
+ if (shooterData.isInMatch()) {
+ shooterData.getMatch().getEntities().add(event.getEntity());
+ shooterData.getMatch().getMatchPlayer(shooter).handleHit();
+ }
+ }
+ }
+ }
+
+ @EventHandler(ignoreCancelled = true)
+ public void onPotionSplash(PotionSplashEvent event) {
+ if (event.getPotion().getShooter() instanceof Player) {
+ final Player shooter = (Player) event.getPotion().getShooter();
+ final PraxiPlayer shooterData = PraxiPlayer.getByUuid(shooter.getUniqueId());
+
+ if (shooterData.isInMatch() && shooterData.getMatch().isFighting()) {
+ if (event.getIntensity(shooter) <= 0.5D) {
+ shooterData.getMatch().getMatchPlayer(shooter).incrementPotionsMissed();
+ }
+ }
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/ServerListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/ServerListener.java
new file mode 100644
index 0000000..5143abd
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/ServerListener.java
@@ -0,0 +1,31 @@
+package me.joeleoli.praxi.listener;
+
+import me.joeleoli.nucleus.Nucleus;
+import me.joeleoli.nucleus.event.PreShutdownEvent;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Material;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+
+public class ServerListener implements Listener {
+
+ @EventHandler
+ public void onPreShutdown(PreShutdownEvent event) {
+ Nucleus.getInstance().getServer().getScheduler().runTaskAsynchronously(Nucleus.getInstance(), () -> {
+ for (Player player : Nucleus.getInstance().getServer().getOnlinePlayers()) {
+ PraxiPlayer.getByUuid(player.getUniqueId()).save();
+ }
+ });
+
+ for (Match match : Match.getMatches()) {
+ match.getPlacedBlocks().forEach(location -> location.getBlock().setType(Material.AIR));
+ match.getChangedBlocks()
+ .forEach((blockState) -> blockState.getLocation().getBlock().setType(blockState.getType()));
+ match.getEntities().forEach(Entity::remove);
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/listener/WorldListener.java b/plugin/src/main/java/me/joeleoli/praxi/listener/WorldListener.java
new file mode 100644
index 0000000..63bfc73
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/listener/WorldListener.java
@@ -0,0 +1,66 @@
+package me.joeleoli.praxi.listener;
+
+import org.bukkit.Difficulty;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBurnEvent;
+import org.bukkit.event.block.BlockIgniteEvent;
+import org.bukkit.event.block.BlockSpreadEvent;
+import org.bukkit.event.block.LeavesDecayEvent;
+import org.bukkit.event.entity.CreatureSpawnEvent;
+import org.bukkit.event.hanging.HangingBreakEvent;
+import org.bukkit.event.weather.WeatherChangeEvent;
+import org.bukkit.event.world.ChunkUnloadEvent;
+import org.bukkit.event.world.WorldLoadEvent;
+
+public class WorldListener implements Listener {
+
+ @EventHandler
+ public void onChunkUnload(ChunkUnloadEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onWorldLoad(WorldLoadEvent event) {
+ event.getWorld().getEntities().clear();
+ event.getWorld().setDifficulty(Difficulty.HARD);
+ }
+
+ @EventHandler
+ public void onWeatherChange(WeatherChangeEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onCreatureSpawn(CreatureSpawnEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onBlockIgnite(BlockIgniteEvent event) {
+ if (event.getCause() == BlockIgniteEvent.IgniteCause.LIGHTNING) {
+ event.setCancelled(true);
+ }
+ }
+
+ @EventHandler
+ public void onLeavesDecay(LeavesDecayEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onHangingBreak(HangingBreakEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onBlockBurn(BlockBurnEvent event) {
+ event.setCancelled(true);
+ }
+
+ @EventHandler
+ public void onBlockSpread(BlockSpreadEvent event) {
+ event.setCancelled(true);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/Match.java b/plugin/src/main/java/me/joeleoli/praxi/match/Match.java
new file mode 100644
index 0000000..4b8510e
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/Match.java
@@ -0,0 +1,722 @@
+package me.joeleoli.praxi.match;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.fairfight.FairFight;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.nametag.NameTagHandler;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.nucleus.util.TimeUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PlayerState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.player.RematchData;
+import me.joeleoli.ragespigot.RageSpigot;
+import me.joeleoli.ragespigot.knockback.KnockbackProfile;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.minecraft.server.v1_8_R3.EntityLightning;
+import net.minecraft.server.v1_8_R3.PacketPlayOutSpawnEntityWeather;
+import org.bukkit.ChatColor;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.Sound;
+import org.bukkit.block.BlockState;
+import org.bukkit.craftbukkit.v1_8_R3.CraftWorld;
+import org.bukkit.craftbukkit.v1_8_R3.entity.CraftPlayer;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.util.Vector;
+
+@Getter
+public abstract class Match {
+
+ protected static final BaseComponent[] HOVER_TEXT =
+ new ChatComponentBuilder(Style.GRAY + "Click to view this player's inventory.").create();
+ @Getter
+ protected static List matches = new ArrayList<>();
+ private UUID matchId = UUID.randomUUID();
+ @Setter
+ private MatchState state = MatchState.STARTING;
+ private UUID queueId;
+ @Setter
+ private Ladder ladder;
+ private Arena arena;
+ private boolean ranked;
+ private List snapshots = new ArrayList<>();
+ private List spectators = new ArrayList<>();
+ private List entities = new ArrayList<>();
+ private List placedBlocks = new ArrayList<>();
+ private List changedBlocks = new ArrayList<>();
+ @Setter
+ private long startTimestamp;
+
+ public Match(Ladder ladder, Arena arena, boolean ranked) {
+ this(null, ladder, arena, ranked);
+ }
+
+ public Match(UUID queueId, Ladder ladder, Arena arena, boolean ranked) {
+ this.queueId = queueId;
+ this.ladder = ladder;
+ this.arena = arena;
+ this.ranked = ranked;
+
+ matches.add(this);
+ }
+
+ public boolean isMatchMakingMatch() {
+ return this.queueId != null;
+ }
+
+ public boolean isStarting() {
+ return this.state == MatchState.STARTING;
+ }
+
+ public boolean isFighting() {
+ return this.state == MatchState.FIGHTING;
+ }
+
+ public boolean isEnding() {
+ return this.state == MatchState.ENDING;
+ }
+
+ public void setupPlayers() {
+ if (this.isSoloMatch()) {
+ final MatchPlayer matchPlayerA = this.getMatchPlayerA();
+ final MatchPlayer matchPlayerB = this.getMatchPlayerB();
+
+ matchPlayerA.setAlive(true);
+ matchPlayerB.setAlive(true);
+
+ final Player playerA = matchPlayerA.toPlayer();
+ final Player playerB = matchPlayerB.toPlayer();
+
+ playerA.showPlayer(playerB);
+ playerB.showPlayer(playerA);
+
+ if (this.arena.getSpawn1().getBlock().getType() == Material.AIR) {
+ playerA.teleport(this.arena.getSpawn1());
+ } else {
+ playerA.teleport(this.arena.getSpawn1().add(0, 2, 0));
+ }
+
+ if (this.arena.getSpawn2().getBlock().getType() == Material.AIR) {
+ playerB.teleport(this.arena.getSpawn2());
+ } else {
+ playerB.teleport(this.arena.getSpawn2().add(0, 2, 0));
+ }
+
+ NameTagHandler.addToTeam(playerA, playerB, ChatColor.RED, this.ladder.isBuild());
+ NameTagHandler.addToTeam(playerB, playerA, ChatColor.RED, this.ladder.isBuild());
+
+ PlayerUtil.reset(playerA);
+ PlayerUtil.reset(playerB);
+
+ playerA.setMaximumNoDamageTicks(this.ladder.getHitDelay());
+ playerB.setMaximumNoDamageTicks(this.ladder.getHitDelay());
+
+ if (this.ladder.isSumo()) {
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(playerA).setAllowTeleport(true);
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(playerB).setAllowTeleport(true);
+
+ PlayerUtil.denyMovement(playerA);
+ PlayerUtil.denyMovement(playerB);
+ } else {
+ for (ItemStack itemStack : PraxiPlayer.getByUuid(playerA.getUniqueId()).getKitItems(this.ladder)) {
+ playerA.getInventory().addItem(itemStack);
+ }
+
+ for (ItemStack itemStack : PraxiPlayer.getByUuid(playerB.getUniqueId()).getKitItems(this.ladder)) {
+ playerB.getInventory().addItem(itemStack);
+ }
+ }
+
+ if (this.ladder.getKbProfile() != null) {
+ final KnockbackProfile profile =
+ RageSpigot.INSTANCE.getConfig().getKbProfileByName(this.ladder.getKbProfile());
+
+ if (profile != null) {
+ playerA.setKnockbackProfile(profile);
+ playerB.setKnockbackProfile(profile);
+ }
+ }
+ } else if (this.isTeamMatch()) {
+ final MatchTeam teamA = this.getTeamA();
+ final MatchTeam teamB = this.getTeamB();
+
+ for (MatchPlayer matchPlayer : teamA.getTeamPlayers()) {
+ if (!matchPlayer.isDisconnected()) {
+ matchPlayer.setAlive(true);
+ }
+
+ final Player player = matchPlayer.toPlayer();
+
+ if (player == null || !player.isOnline()) {
+ continue;
+ }
+
+ player.teleport(this.arena.getSpawn1());
+
+ for (Player member : teamA.getPlayers()) {
+ NameTagHandler.addToTeam(player, member, ChatColor.GREEN, this.ladder.isBuild());
+ }
+
+ for (Player enemy : teamB.getPlayers()) {
+ NameTagHandler.addToTeam(player, enemy, ChatColor.RED, this.ladder.isBuild());
+ }
+ }
+
+ for (MatchPlayer matchPlayer : teamB.getTeamPlayers()) {
+ if (!matchPlayer.isDisconnected()) {
+ matchPlayer.setAlive(true);
+ }
+
+ final Player player = matchPlayer.toPlayer();
+
+ if (player == null || !player.isOnline()) {
+ continue;
+ }
+
+ player.teleport(this.arena.getSpawn2());
+
+ for (Player member : teamB.getPlayers()) {
+ NameTagHandler.addToTeam(player, member, ChatColor.GREEN, this.ladder.isBuild());
+ }
+
+ for (Player enemy : teamA.getPlayers()) {
+ NameTagHandler.addToTeam(player, enemy, ChatColor.RED, this.ladder.isBuild());
+ }
+ }
+
+ final List players = this.getPlayers();
+
+ for (Player first : players) {
+ PlayerUtil.reset(first);
+
+ first.setMaximumNoDamageTicks(this.ladder.getHitDelay());
+
+ if (this.ladder.isSumo()) {
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(first).setAllowTeleport(true);
+
+ PlayerUtil.denyMovement(first);
+ } else {
+ for (ItemStack itemStack : PraxiPlayer.getByUuid(first.getUniqueId()).getKitItems(this.ladder)) {
+ first.getInventory().addItem(itemStack);
+ }
+ }
+
+ if (this.ladder.getKbProfile() != null) {
+ final KnockbackProfile profile =
+ RageSpigot.INSTANCE.getConfig().getKbProfileByName(this.ladder.getKbProfile());
+
+ if (profile != null) {
+ first.setKnockbackProfile(profile);
+ }
+ }
+
+ for (Player second : players) {
+ if (first.getUniqueId().equals(second.getUniqueId())) {
+ continue;
+ }
+
+ first.showPlayer(second);
+ second.showPlayer(first);
+ }
+ }
+ }
+ }
+
+ public void handleStart() {
+ this.setupPlayers();
+
+ this.state = MatchState.STARTING;
+ this.startTimestamp = -1;
+ this.arena.setActive(true);
+
+ this.onStart();
+
+ this.getPlayers().forEach(player -> {
+ player.sendMessage(Style.YELLOW + "You are playing on arena " + Style.PINK + this.arena.getName() + Style.YELLOW + ".");
+ });
+
+ TaskUtil.runTimer(new MatchStartTask(this), 20L, 20L);
+ }
+
+ private void handleEnd() {
+ this.state = MatchState.ENDING;
+
+ this.onEnd();
+
+ if (this.isSoloMatch()) {
+ final Player playerA = this.getPlayerA();
+ final Player playerB = this.getPlayerB();
+
+ playerA.hidePlayer(playerB);
+ playerB.hidePlayer(playerA);
+
+ for (MatchPlayer matchPlayer : new MatchPlayer[]{ this.getMatchPlayerA(), this.getMatchPlayerB() }) {
+ if (matchPlayer.isAlive()) {
+ final Player player = matchPlayer.toPlayer();
+
+ if (player != null) {
+ player.setFireTicks(0);
+ player.updateInventory();
+
+ if (matchPlayer.isAlive()) {
+ MatchSnapshot snapshot = new MatchSnapshot(matchPlayer);
+
+ snapshot.setSwitchTo(this.getOpponentMatchPlayer(player));
+
+ this.snapshots.add(snapshot);
+ }
+ }
+ }
+ }
+ } else if (this.isTeamMatch()) {
+ for (MatchPlayer firstMatchPlayer : this.getMatchPlayers()) {
+ if (firstMatchPlayer.isDisconnected()) {
+ continue;
+ }
+
+ final Player player = firstMatchPlayer.toPlayer();
+
+ if (player != null) {
+ for (MatchPlayer secondMatchPlayer : this.getMatchPlayers()) {
+ if (secondMatchPlayer.isDisconnected()) {
+ continue;
+ }
+
+ if (secondMatchPlayer.getUuid().equals(player.getUniqueId())) {
+ continue;
+ }
+
+ final Player secondPlayer = secondMatchPlayer.toPlayer();
+
+ if (secondPlayer == null) {
+ continue;
+ }
+
+ player.hidePlayer(secondPlayer);
+ }
+
+ player.setFireTicks(0);
+ player.updateInventory();
+
+ if (firstMatchPlayer.isAlive()) {
+ this.snapshots.add(new MatchSnapshot(firstMatchPlayer));
+ }
+ }
+ }
+ }
+
+ this.getSpectators().forEach(this::removeSpectator);
+ this.entities.forEach(Entity::remove);
+ this.snapshots.forEach(matchInventory -> {
+ matchInventory.setCreated(System.currentTimeMillis());
+ MatchSnapshot.getCache().put(matchInventory.getMatchPlayer().getUuid(), matchInventory);
+ });
+
+ new MatchResetTask(this).runTask(Praxi.getInstance());
+
+ TaskUtil.runLater(() -> {
+ if (this.isSoloMatch()) {
+ final UUID rematchKey = UUID.randomUUID();
+ final Player playerA = this.getPlayerA();
+ final Player playerB = this.getPlayerB();
+
+ if (playerA != null && playerB != null) {
+ NameTagHandler.removeFromTeams(playerA, playerB);
+ NameTagHandler.removeFromTeams(playerB, playerA);
+ NameTagHandler.removeHealthDisplay(playerA);
+ NameTagHandler.removeHealthDisplay(playerB);
+ }
+
+ for (MatchPlayer matchPlayer : new MatchPlayer[]{ this.getMatchPlayerA(), this.getMatchPlayerB() }) {
+ final Player player = matchPlayer.toPlayer();
+ final Player opponent = this.getOpponentPlayer(player);
+
+ if (player != null) {
+ player.setKnockbackProfile(null);
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (opponent != null) {
+ praxiPlayer.setRematchData(
+ new RematchData(rematchKey, player.getUniqueId(), opponent.getUniqueId(),
+ this.getLadder(), this.getArena()
+ ));
+ }
+
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.setMatch(null);
+ praxiPlayer.loadHotbar();
+
+ PlayerUtil.spawn(player);
+ }
+ }
+ } else if (this.isTeamMatch()) {
+ this.getPlayers().forEach(player -> {
+ NameTagHandler.removeHealthDisplay(player);
+ this.getPlayers().forEach(otherPlayer -> NameTagHandler.removeFromTeams(player, otherPlayer));
+ });
+
+ for (MatchPlayer matchPlayer : this.getMatchPlayers()) {
+ if (matchPlayer.isDisconnected()) {
+ continue;
+ }
+
+ final Player player = matchPlayer.toPlayer();
+
+ if (player != null) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.setMatch(null);
+ praxiPlayer.loadHotbar();
+
+ player.setKnockbackProfile(null);
+
+ PlayerUtil.spawn(player);
+ }
+ }
+ }
+ }, 20L * 3);
+
+ matches.remove(this);
+ }
+
+ public void handleRespawn(Player player) {
+ player.spigot().respawn();
+ player.setVelocity(new Vector());
+
+ this.onRespawn(player);
+ }
+
+ public void handleDeath(Player deadPlayer, Player killerPlayer, boolean disconnected) {
+ final MatchPlayer matchPlayer = this.getMatchPlayer(deadPlayer);
+
+ if (!matchPlayer.isAlive()) {
+ return;
+ }
+
+ matchPlayer.setAlive(false);
+ matchPlayer.setDisconnected(disconnected);
+
+ final List involvedPlayers = new ArrayList<>();
+
+ if (this.isSoloMatch()) {
+ involvedPlayers.add(this.getPlayerA());
+ involvedPlayers.add(this.getPlayerB());
+ } else {
+ involvedPlayers.addAll(this.getPlayers());
+ }
+
+ involvedPlayers.addAll(this.getSpectators());
+
+ EntityLightning is =
+ new EntityLightning(((CraftWorld) deadPlayer.getWorld()).getHandle(), deadPlayer.getLocation().getX(),
+ deadPlayer.getLocation().getY(), deadPlayer.getLocation().getZ()
+ );
+ PacketPlayOutSpawnEntityWeather lightningPacket = new PacketPlayOutSpawnEntityWeather(is);
+
+ involvedPlayers.forEach(other -> {
+ if (other != null) {
+ other.playSound(deadPlayer.getLocation(), Sound.AMBIENCE_THUNDER, 1.0F, 1.0F);
+ ((CraftPlayer) other).getHandle().playerConnection.sendPacket(lightningPacket);
+ }
+ });
+
+ for (Player involved : involvedPlayers) {
+ String deadName = Style.RED + deadPlayer.getName();
+
+ if (this.isSoloMatch()) {
+ // Todo: fix NPE here, idk where but it says the line right below...
+ // DEBUG FOR NOW:
+
+ if (deadPlayer == null) {
+ System.out.println("DEBUG: DEAD PLAYER NULL");
+ continue;
+ }
+
+ if (involved == null) {
+ System.out.println("DEBUG: INVOLVED PLAYER NULL");
+ continue;
+ }
+
+ if (deadPlayer.getUniqueId().equals(involved.getUniqueId())) {
+ deadName = Style.GREEN + deadPlayer.getName();
+ }
+ } else {
+ final MatchTeam matchTeam = this.getTeam(involved);
+
+ if (matchTeam != null && matchTeam.containsPlayer(deadPlayer)) {
+ deadName = Style.GREEN + deadPlayer.getName();
+ }
+ }
+
+ if (matchPlayer.isDisconnected()) {
+ involved.sendMessage(deadName + Style.YELLOW + " has disconnected.");
+ continue;
+ }
+
+ String killerName = null;
+
+ if (killerPlayer != null) {
+ killerName = Style.RED + killerPlayer.getName();
+
+ if (this.isSoloMatch()) {
+ if (killerPlayer.getUniqueId().equals(involved.getUniqueId())) {
+ killerName = Style.GREEN + killerPlayer.getName();
+ }
+ } else {
+ final MatchTeam matchTeam = this.getTeam(involved);
+
+ if (matchTeam != null && matchTeam.containsPlayer(killerPlayer)) {
+ killerName = Style.GREEN + killerPlayer.getName();
+ }
+ }
+ }
+
+ if (killerName == null) {
+ involved.sendMessage(deadName + Style.YELLOW + " has died.");
+ } else {
+ involved.sendMessage(deadName + Style.YELLOW + " was killed by " + killerName + Style.YELLOW + ".");
+ }
+ }
+
+ this.onDeath(deadPlayer, killerPlayer);
+
+ if (this.canEnd()) {
+ this.handleEnd();
+ }
+ }
+
+ public String getDuration() {
+ if (this.isStarting()) {
+ return "00:00";
+ } else if (this.isEnding()) {
+ return "Ending";
+ } else {
+ return TimeUtil.millisToTimer(this.getElapsedDuration());
+ }
+ }
+
+ public long getElapsedDuration() {
+ return System.currentTimeMillis() - this.startTimestamp;
+ }
+
+ public void broadcastMessage(String message) {
+ this.getPlayers().forEach(player -> player.sendMessage(message));
+ this.getSpectators().forEach(player -> player.sendMessage(message));
+ }
+
+ public void broadcastSound(Sound sound) {
+ this.getPlayers().forEach(player -> player.playSound(player.getLocation(), sound, 1.0F, 1.0F));
+ this.getSpectators().forEach(player -> player.playSound(player.getLocation(), sound, 1.0F, 1.0F));
+ }
+
+ public List getInvolvedPlayers() {
+ List toReturn = new ArrayList<>();
+
+ toReturn.addAll(this.spectators);
+
+ if (this.isSoloMatch()) {
+ toReturn.add(this.getMatchPlayerA().getUuid());
+ toReturn.add(this.getMatchPlayerB().getUuid());
+ } else if (this.isTeamMatch()) {
+ this.getMatchPlayers().forEach(matchPlayer -> toReturn.add(matchPlayer.getUuid()));
+ }
+
+ return toReturn;
+ }
+
+ protected List getSpectators() {
+ return PlayerUtil.convertUUIDListToPlayerList(this.spectators);
+ }
+
+ public void addSpectator(Player player, Player target) {
+ this.spectators.add(player.getUniqueId());
+
+ if (this.isSoloMatch()) {
+ final Player playerA = this.getPlayerA();
+ final Player playerB = this.getPlayerB();
+
+ if (playerA != null) {
+ player.showPlayer(playerA);
+
+ NameTagHandler.addToTeam(player, playerA, ChatColor.AQUA, this.ladder.isBuild());
+ }
+
+ if (playerB != null) {
+ player.showPlayer(playerB);
+
+ NameTagHandler.addToTeam(player, playerB, ChatColor.LIGHT_PURPLE, this.ladder.isBuild());
+ }
+ } else if (this.isTeamMatch()) {
+ this.getTeamA().getPlayers().forEach(teamPlayer -> {
+ player.showPlayer(teamPlayer);
+ teamPlayer.hidePlayer(player);
+ NameTagHandler.addToTeam(player, teamPlayer, ChatColor.AQUA, this.ladder.isBuild());
+ });
+
+ this.getTeamB().getPlayers().forEach(teamPlayer -> {
+ player.showPlayer(teamPlayer);
+ teamPlayer.hidePlayer(player);
+ NameTagHandler.addToTeam(player, teamPlayer, ChatColor.LIGHT_PURPLE, this.ladder.isBuild());
+ });
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setMatch(this);
+ praxiPlayer.setState(PlayerState.SPECTATE_MATCH);
+ praxiPlayer.loadHotbar();
+
+ player.setAllowFlight(true);
+ player.setFlying(true);
+ player.updateInventory();
+ player.teleport(target.getLocation().clone().add(0, 2, 0));
+ player.sendMessage(Style.YELLOW + "You are spectating " + Style.PINK + target.getName() + Style.YELLOW + ".");
+
+ if (this.isSoloMatch()) {
+ for (Player matchPlayer : new Player[]{ this.getPlayerA(), this.getPlayerB() }) {
+ if (!player.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.PINK + player.getName() + Style.YELLOW + " is now spectating.");
+
+ } else if (matchPlayer.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.GRAY + "[Silent] " + Style.PINK + player.getName() + Style.YELLOW +
+ " is now spectating.");
+ }
+ }
+ } else if (this.isTeamMatch()) {
+ for (Player matchPlayer : this.getPlayers()) {
+ if (!player.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.PINK + player.getName() + Style.YELLOW + " is now spectating.");
+
+ } else if (matchPlayer.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.GRAY + "[Silent] " + Style.PINK + player.getName() + Style.YELLOW +
+ " is now spectating.");
+ }
+ }
+ }
+ }
+
+ public void removeSpectator(Player player) {
+ this.spectators.remove(player.getUniqueId());
+
+ if (this.isSoloMatch()) {
+ player.hidePlayer(this.getPlayerA());
+ player.hidePlayer(this.getPlayerB());
+
+ NameTagHandler.removeFromTeams(player, this.getPlayerA());
+ NameTagHandler.removeFromTeams(player, this.getPlayerB());
+ NameTagHandler.removeHealthDisplay(player);
+ } else if (this.isTeamMatch()) {
+ this.getPlayers().forEach(other -> {
+ player.hidePlayer(other);
+ other.hidePlayer(player);
+ NameTagHandler.removeFromTeams(player, other);
+ });
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (this.state != MatchState.ENDING) {
+ final String toSend = Style.PINK + player.getName() + Style.YELLOW + " is no longer spectating your match.";
+
+ if (this.isSoloMatch()) {
+ for (Player matchPlayer : new Player[]{ this.getPlayerA(), this.getPlayerB() }) {
+ if (!player.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(toSend);
+ } else if (matchPlayer.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.GRAY + "[Silent] " + toSend);
+ }
+ }
+ } else if (this.isTeamMatch()) {
+ for (Player matchPlayer : this.getPlayers()) {
+ if (!player.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(toSend);
+ } else if (matchPlayer.hasPermission("praxi.spectate.hidden")) {
+ matchPlayer.sendMessage(Style.GRAY + "[Silent] " + toSend);
+ }
+ }
+ }
+ }
+
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.setMatch(null);
+ praxiPlayer.loadHotbar();
+
+ PlayerUtil.spawn(player);
+ }
+
+ public abstract boolean isDuel();
+
+ public abstract boolean isSoloMatch();
+
+ public abstract boolean isTeamMatch();
+
+ public abstract void onStart();
+
+ public abstract void onEnd();
+
+ public abstract void onDeath(Player player, Player killer);
+
+ public abstract void onRespawn(Player player);
+
+ public abstract boolean canEnd();
+
+ public abstract Player getWinningPlayer();
+
+ public abstract MatchTeam getWinningTeam();
+
+ public abstract MatchPlayer getMatchPlayerA();
+
+ public abstract MatchPlayer getMatchPlayerB();
+
+ public abstract List getMatchPlayers();
+
+ public abstract Player getPlayerA();
+
+ public abstract Player getPlayerB();
+
+ public abstract List getPlayers();
+
+ public abstract MatchTeam getTeamA();
+
+ public abstract MatchTeam getTeamB();
+
+ public abstract MatchTeam getTeam(MatchPlayer matchPlayer);
+
+ public abstract MatchTeam getTeam(Player player);
+
+ public abstract MatchPlayer getMatchPlayer(Player player);
+
+ public abstract int getOpponentsLeft(Player player);
+
+ public abstract MatchTeam getOpponentTeam(MatchTeam matchTeam);
+
+ public abstract MatchTeam getOpponentTeam(Player player);
+
+ public abstract MatchPlayer getOpponentMatchPlayer(Player player);
+
+ public abstract Player getOpponentPlayer(Player player);
+
+ public abstract int getTotalRoundWins();
+
+ public abstract int getRoundWins(MatchPlayer matchPlayer);
+
+ public abstract int getRoundWins(MatchTeam matchTeam);
+
+ public abstract int getRoundsNeeded(MatchPlayer matchPlayer);
+
+ public abstract int getRoundsNeeded(MatchTeam matchTeam);
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchPlayer.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchPlayer.java
new file mode 100644
index 0000000..bcef4fa
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchPlayer.java
@@ -0,0 +1,51 @@
+package me.joeleoli.praxi.match;
+
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.team.TeamPlayer;
+import org.bukkit.entity.Player;
+
+@Getter
+@Setter
+public class MatchPlayer extends TeamPlayer {
+
+ private boolean alive = true;
+ private boolean disconnected;
+ private int elo, potionsThrown, potionsMissed, hits, combo, longestCombo;
+
+ public MatchPlayer(Player player) {
+ super(player.getUniqueId(), player.getName());
+ }
+
+ public double getPotionAccuracy() {
+ if (this.potionsMissed == 0) {
+ return 100.0;
+ } else if (this.potionsThrown == this.potionsMissed) {
+ return 50.0;
+ }
+
+ return Math.round(100.0D - (((double) this.potionsMissed / (double) this.potionsThrown) * 100.0D));
+ }
+
+ public void incrementPotionsThrown() {
+ this.potionsThrown++;
+ }
+
+ public void incrementPotionsMissed() {
+ this.potionsMissed++;
+ }
+
+ public void handleHit() {
+ this.hits++;
+ this.combo++;
+
+ if (this.combo > this.longestCombo) {
+ this.longestCombo = this.combo;
+ }
+ }
+
+ public void resetCombo() {
+ this.combo = 0;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchResetTask.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchResetTask.java
new file mode 100644
index 0000000..2f7fc47
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchResetTask.java
@@ -0,0 +1,82 @@
+package me.joeleoli.praxi.match;
+
+import com.boydti.fawe.util.EditSessionBuilder;
+import com.boydti.fawe.util.TaskManager;
+import com.sk89q.worldedit.EditSession;
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldedit.blocks.BaseBlock;
+import lombok.AllArgsConstructor;
+import org.bukkit.Location;
+import org.bukkit.block.BlockState;
+import org.bukkit.scheduler.BukkitRunnable;
+
+@AllArgsConstructor
+public class MatchResetTask extends BukkitRunnable {
+
+ private Match match;
+
+ @Override
+ public void run() {
+ if (this.match.getLadder().isBuild() && this.match.getPlacedBlocks().size() > 0) {
+ TaskManager.IMP.async(() -> {
+ EditSession editSession = new EditSessionBuilder(this.match.getArena().getSpawn1().getWorld().getName())
+ .fastmode(true)
+ .allowedRegionsEverywhere()
+ .autoQueue(false)
+ .limitUnlimited()
+ .build();
+
+ for (Location location : this.match.getPlacedBlocks()) {
+ try {
+ editSession.setBlock(
+ new Vector((double) location.getBlockX(), (double) location.getBlockY(),
+ location.getZ()
+ ), new BaseBlock(0));
+ } catch (Exception ex) {
+ }
+ }
+
+ editSession.flushQueue();
+
+ TaskManager.IMP.task(() -> {
+ this.match.getPlacedBlocks().clear();
+ this.match.getArena().setActive(false);
+ this.cancel();
+ });
+ });
+ } else if (this.match.getLadder().isBuild() && this.match.getChangedBlocks().size() > 0) {
+ TaskManager.IMP.async(() -> {
+ EditSession editSession = new EditSessionBuilder(this.match.getArena().getSpawn1().getWorld().getName())
+ .fastmode(true)
+ .allowedRegionsEverywhere()
+ .autoQueue(false)
+ .limitUnlimited()
+ .build();
+
+ for (BlockState blockState : this.match.getChangedBlocks()) {
+ try {
+ editSession.setBlock(
+ new Vector(blockState.getLocation().getBlockX(), blockState.getLocation().getBlockY(),
+ blockState.getLocation().getZ()
+ ), new BaseBlock(blockState.getTypeId(), blockState.getRawData()));
+ } catch (Exception ex) {
+ }
+ }
+
+ editSession.flushQueue();
+
+ TaskManager.IMP.task(() -> {
+ if (this.match.getLadder().isBuild()) {
+ this.match.getChangedBlocks().clear();
+ this.match.getArena().setActive(false);
+ }
+
+ this.cancel();
+ });
+ });
+ } else {
+ this.cancel();
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchSnapshot.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchSnapshot.java
new file mode 100644
index 0000000..b860979
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchSnapshot.java
@@ -0,0 +1,77 @@
+package me.joeleoli.praxi.match;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Data;
+import lombok.Getter;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.potion.PotionEffect;
+
+@Data
+public class MatchSnapshot {
+
+ @Getter
+ private static Map cache = new HashMap<>();
+
+ private MatchPlayer matchPlayer;
+ private MatchPlayer switchTo;
+ private int health;
+ private int hunger;
+ private ItemStack[] armor;
+ private ItemStack[] contents;
+ private Collection effects;
+ private long created = System.currentTimeMillis();
+
+ public MatchSnapshot(MatchPlayer matchPlayer) {
+ this(matchPlayer, null);
+ }
+
+ public MatchSnapshot(MatchPlayer matchPlayer, MatchPlayer switchTo) {
+ this.matchPlayer = matchPlayer;
+
+ final Player player = this.matchPlayer.toPlayer();
+
+ this.health = player.getHealth() == 0 ? 0 : (int) Math.round(player.getHealth() / 2);
+ this.hunger = player.getFoodLevel();
+ this.armor = player.getInventory().getArmorContents();
+ this.contents = player.getInventory().getContents();
+ this.effects = player.getActivePotionEffects();
+ this.switchTo = switchTo;
+ }
+
+ public static MatchSnapshot getByUuid(UUID uuid) {
+ return cache.get(uuid);
+ }
+
+ public static MatchSnapshot getByName(String name) {
+ for (MatchSnapshot details : cache.values()) {
+ if (details.getMatchPlayer().getName().equalsIgnoreCase(name)) {
+ return details;
+ }
+ }
+
+ return null;
+ }
+
+ public int getRemainingPotions() {
+ int amount = 0;
+
+ for (ItemStack itemStack : this.contents) {
+ if (itemStack != null && itemStack.getType() == Material.POTION && itemStack.getDurability() == 16421) {
+ amount++;
+ }
+ }
+
+ return amount;
+ }
+
+ public boolean shouldDisplayRemainingPotions() {
+ return this.getRemainingPotions() > 0 || this.matchPlayer.getPotionsThrown() > 0 ||
+ this.matchPlayer.getPotionsMissed() > 0;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchStartTask.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchStartTask.java
new file mode 100644
index 0000000..6a5b311
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchStartTask.java
@@ -0,0 +1,90 @@
+package me.joeleoli.praxi.match;
+
+import me.joeleoli.fairfight.FairFight;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import org.bukkit.Sound;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
+
+public class MatchStartTask extends BukkitRunnable {
+
+ private Match match;
+ private int ticks;
+
+ public MatchStartTask(Match match) {
+ this.match = match;
+ }
+
+ @Override
+ public void run() {
+ int seconds = 5 - this.ticks;
+
+ if (this.match.isEnding()) {
+ this.cancel();
+ return;
+ }
+
+ if (this.match.getLadder().isSumo()) {
+ if (seconds == 2) {
+ if (this.match.isSoloMatch()) {
+ final Player playerA = this.match.getPlayerA();
+ final Player playerB = this.match.getPlayerB();
+
+ if (playerA != null) {
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(playerA).setAllowTeleport(false);
+
+ PlayerUtil.allowMovement(playerA);
+ }
+
+ if (playerB != null) {
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(playerB).setAllowTeleport(false);
+
+ PlayerUtil.allowMovement(playerB);
+ }
+ } else if (this.match.isTeamMatch()) {
+ this.match.getMatchPlayers().forEach(matchPlayer -> {
+ if (!matchPlayer.isDisconnected()) {
+ final Player player = matchPlayer.toPlayer();
+
+ if (player != null) {
+ FairFight.getInstance().getPlayerDataManager().getPlayerData(player)
+ .setAllowTeleport(false);
+
+ PlayerUtil.allowMovement(player);
+ }
+ }
+ });
+ }
+
+ this.match.setState(MatchState.FIGHTING);
+ this.match.setStartTimestamp(System.currentTimeMillis());
+ this.match.broadcastMessage(Style.YELLOW + "The round has started!");
+ this.match.broadcastSound(Sound.NOTE_BASS);
+ this.cancel();
+ return;
+ }
+
+ this.match.broadcastMessage(
+ Style.YELLOW + "The round will start in " + Style.PINK + (seconds - 2) + " second" +
+ (seconds - 2 == 1 ? "" : "s") + Style.YELLOW + "...");
+ this.match.broadcastSound(Sound.NOTE_PLING);
+ } else {
+ if (seconds == 0) {
+ this.match.setState(MatchState.FIGHTING);
+ this.match.setStartTimestamp(System.currentTimeMillis());
+ this.match.broadcastMessage(Style.YELLOW + "The match has started!");
+ this.match.broadcastSound(Sound.NOTE_BASS);
+ this.cancel();
+ return;
+ }
+
+ this.match.broadcastMessage(Style.YELLOW + "The match will start in " + Style.PINK + seconds + " second" +
+ (seconds == 1 ? "" : "s") + Style.YELLOW + "...");
+ this.match.broadcastSound(Sound.NOTE_PLING);
+ }
+
+ this.ticks++;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchState.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchState.java
new file mode 100644
index 0000000..f599817
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchState.java
@@ -0,0 +1,9 @@
+package me.joeleoli.praxi.match;
+
+public enum MatchState {
+
+ STARTING,
+ FIGHTING,
+ ENDING
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/MatchTeam.java b/plugin/src/main/java/me/joeleoli/praxi/match/MatchTeam.java
new file mode 100644
index 0000000..6f9d809
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/MatchTeam.java
@@ -0,0 +1,23 @@
+package me.joeleoli.praxi.match;
+
+import me.joeleoli.nucleus.team.Team;
+
+public class MatchTeam extends Team {
+
+ public MatchTeam(MatchPlayer matchPlayer) {
+ super(matchPlayer);
+ }
+
+ public int getDisconnectedCount() {
+ int disconnected = 0;
+
+ for (MatchPlayer matchPlayer : this.getTeamPlayers()) {
+ if (matchPlayer.isDisconnected()) {
+ disconnected++;
+ }
+ }
+
+ return disconnected;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/gui/MatchDetailsMenu.java b/plugin/src/main/java/me/joeleoli/praxi/match/gui/MatchDetailsMenu.java
new file mode 100644
index 0000000..c320133
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/gui/MatchDetailsMenu.java
@@ -0,0 +1,210 @@
+package me.joeleoli.praxi.match.gui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.command.CommandHandler;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.menu.buttons.DisplayButton;
+import me.joeleoli.nucleus.util.BukkitUtil;
+import me.joeleoli.nucleus.util.InventoryUtil;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TimeUtil;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.MatchSnapshot;
+import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.potion.PotionEffect;
+
+@AllArgsConstructor
+public class MatchDetailsMenu extends Menu {
+
+ private MatchSnapshot snapshot;
+
+ @Override
+ public String getTitle(Player player) {
+ return ChatColor.YELLOW + "Snapshot of " + this.snapshot.getMatchPlayer().getName();
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final Map buttons = new HashMap<>();
+ final ItemStack[] fixedContents = InventoryUtil.fixInventoryOrder(this.snapshot.getContents());
+
+ for (int i = 0; i < fixedContents.length; i++) {
+ final ItemStack itemStack = fixedContents[i];
+
+ if (itemStack == null || itemStack.getType() == Material.AIR) {
+ continue;
+ }
+
+ buttons.put(i, new DisplayButton(itemStack, true));
+ }
+
+ for (int i = 0; i < this.snapshot.getArmor().length; i++) {
+ ItemStack itemStack = this.snapshot.getArmor()[i];
+
+ if (itemStack != null && itemStack.getType() != Material.AIR) {
+ buttons.put(39 - i, new DisplayButton(itemStack, true));
+ }
+ }
+
+ int pos = 45;
+
+ buttons.put(pos++, new HealthButton(this.snapshot.getHealth()));
+ buttons.put(pos++, new HungerButton(this.snapshot.getHunger()));
+ buttons.put(pos++, new EffectsButton(this.snapshot.getEffects()));
+
+ if (this.snapshot.shouldDisplayRemainingPotions()) {
+ buttons.put(
+ pos++,
+ new PotionsButton(this.snapshot.getMatchPlayer().getName(), this.snapshot.getRemainingPotions())
+ );
+ }
+
+ buttons.put(pos, new StatisticsButton(this.snapshot.getMatchPlayer()));
+
+ if (this.snapshot.getSwitchTo() != null) {
+ buttons.put(53, new SwitchInventoryButton(this.snapshot.getSwitchTo()));
+ }
+
+ return buttons;
+ }
+
+ @Override
+ public void onOpen(Player player) {
+ player.sendMessage(Style.YELLOW + "You are viewing " + Style.PINK + this.snapshot.getMatchPlayer().getName() +
+ Style.YELLOW + "'s inventory.");
+ }
+
+ @AllArgsConstructor
+ private class SwitchInventoryButton extends Button {
+
+ private MatchPlayer switchTo;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.LEVER)
+ .name(Style.YELLOW + Style.BOLD + "Opponent's Inventory")
+ .lore(Style.YELLOW + "Switch to " + Style.PINK + this.switchTo.getName() + Style.YELLOW +
+ "'s inventory")
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hb) {
+ CommandHandler.executeCommand(player, "viewinv " + this.switchTo.getUuid().toString());
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class HealthButton extends Button {
+
+ private int health;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.MELON)
+ .name(Style.YELLOW + Style.BOLD + "Health: " + Style.PINK + this.health + "/10 " + Style.UNICODE_HEART)
+ .amount(this.health == 0 ? 1 : this.health)
+ .build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class HungerButton extends Button {
+
+ private int hunger;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.COOKED_BEEF)
+ .name(Style.YELLOW + Style.BOLD + "Hunger: " + Style.PINK + this.hunger + "/20")
+ .amount(this.hunger == 0 ? 1 : this.hunger)
+ .build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class EffectsButton extends Button {
+
+ private Collection effects;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ final ItemBuilder builder = new ItemBuilder(Material.POTION)
+ .name(Style.YELLOW + Style.BOLD + "Potion Effects");
+
+ if (this.effects.isEmpty()) {
+ builder.lore(Style.PINK + "No potion effects");
+ } else {
+ final List lore = new ArrayList<>();
+
+ this.effects.forEach(effect -> {
+ final String name = BukkitUtil.getName(effect.getType()) + " " + (effect.getAmplifier() + 1);
+ final String duration = " (" + TimeUtil.millisToTimer((effect.getDuration() / 20) * 1000) + ")";
+
+ lore.add(Style.PINK + name + Style.GRAY + duration);
+ });
+
+ builder.lore(lore);
+ }
+
+ return builder.build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class PotionsButton extends Button {
+
+ private String name;
+ private int potions;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.POTION)
+ .durability(16421)
+ .amount(this.potions == 0 ? 1 : this.potions)
+ .name(Style.YELLOW + Style.BOLD + "Potions")
+ .lore(Style.PINK + this.name + Style.YELLOW + " had " + Style.PINK + this.potions + Style.YELLOW +
+ " potion" + (this.potions == 1 ? "" : "s") + " left.")
+ .build();
+ }
+
+ }
+
+ @AllArgsConstructor
+ private class StatisticsButton extends Button {
+
+ private MatchPlayer matchPlayer;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(Material.PAPER)
+ .name(Style.YELLOW + Style.BOLD + "Statistics")
+ .lore(Arrays.asList(
+ Style.PINK + "Hits: " + Style.RESET + this.matchPlayer.getHits(),
+ Style.PINK + "Longest Combo: " + Style.RESET + this.matchPlayer.getLongestCombo(),
+ Style.PINK + "Potions Thrown: " + Style.RESET + this.matchPlayer.getPotionsThrown(),
+ Style.PINK + "Potions Missed: " + Style.RESET + this.matchPlayer.getPotionsMissed(),
+ Style.PINK + "Potion Accuracy: " + Style.RESET + this.matchPlayer.getPotionAccuracy()
+ ))
+ .build();
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/impl/SoloMatch.java b/plugin/src/main/java/me/joeleoli/praxi/match/impl/SoloMatch.java
new file mode 100644
index 0000000..8508beb
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/impl/SoloMatch.java
@@ -0,0 +1,375 @@
+package me.joeleoli.praxi.match.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import lombok.Getter;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.elo.EloUtil;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.MatchSnapshot;
+import me.joeleoli.praxi.match.MatchState;
+import me.joeleoli.praxi.match.MatchTeam;
+import me.joeleoli.praxi.player.PlayerState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.bukkit.entity.Player;
+
+@Getter
+public class SoloMatch extends Match {
+
+ private MatchPlayer playerA;
+ private MatchPlayer playerB;
+ private int playerARoundWins = 0;
+ private int playerBRoundWins = 0;
+ private boolean duel;
+
+ public SoloMatch(MatchPlayer playerA, MatchPlayer playerB, Ladder ladder, Arena arena, boolean ranked,
+ boolean duel) {
+ this(null, playerA, playerB, ladder, arena, ranked, duel);
+
+ this.duel = duel;
+ }
+
+ public SoloMatch(UUID queueId, MatchPlayer playerA, MatchPlayer playerB, Ladder ladder, Arena arena, boolean ranked,
+ boolean duel) {
+ super(queueId, ladder, arena, ranked);
+
+ this.playerA = playerA;
+ this.playerB = playerB;
+ this.duel = duel;
+ }
+
+ @Override
+ public boolean isSoloMatch() {
+ return true;
+ }
+
+ @Override
+ public boolean isTeamMatch() {
+ return false;
+ }
+
+ @Override
+ public void onStart() {
+ if (this.getTotalRoundWins() == 0) {
+ for (MatchPlayer matchPlayer : new MatchPlayer[]{ this.playerA, this.playerB }) {
+ final Player player = matchPlayer.toPlayer();
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setState(PlayerState.IN_MATCH);
+ praxiPlayer.setMatch(this);
+ }
+ }
+ }
+
+ @Override
+ public void onEnd() {
+ final Player winningPlayer = this.getWinningPlayer();
+ final Player losingPlayer = this.getOpponentPlayer(winningPlayer);
+ final MatchPlayer winningMatchPlayer = this.getMatchPlayer(winningPlayer);
+ final MatchPlayer losingMatchPlayer = this.getMatchPlayer(losingPlayer);
+ final HoverEvent winnerHoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, HOVER_TEXT);
+ final ClickEvent winnerClickEvent =
+ new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/viewinv " + winningPlayer.getUniqueId().toString());
+ final HoverEvent loserHoverEvent = new HoverEvent(HoverEvent.Action.SHOW_TEXT, HOVER_TEXT);
+ final ClickEvent loserClickEvent =
+ new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/viewinv " + losingPlayer.getUniqueId().toString());
+ final ChatComponentBuilder inventoriesBuilder = new ChatComponentBuilder("");
+
+ inventoriesBuilder
+ .append("Winner: ")
+ .color(ChatColor.GREEN)
+ .append(winningPlayer.getName())
+ .color(ChatColor.YELLOW);
+ inventoriesBuilder
+ .setCurrentHoverEvent(winnerHoverEvent)
+ .setCurrentClickEvent(winnerClickEvent)
+ .append(" - ")
+ .color(ChatColor.GRAY)
+ .append("Loser: ")
+ .color(ChatColor.RED)
+ .append(losingPlayer.getName())
+ .color(ChatColor.YELLOW);
+ inventoriesBuilder
+ .setCurrentHoverEvent(loserHoverEvent)
+ .setCurrentClickEvent(loserClickEvent);
+
+ final List components = new ArrayList<>();
+
+ components.add(new ChatComponentBuilder("").parse("&dPost-Match Inventories &7(click name to view)").create());
+ components.add(inventoriesBuilder.create());
+
+ if (this.isRanked()) {
+ final int oldWinnerElo = winningMatchPlayer.getElo();
+ final int oldLoserElo = losingMatchPlayer.getElo();
+ final int newWinnerElo = EloUtil.getNewRating(oldWinnerElo, oldLoserElo, true);
+ final int newLoserElo = EloUtil.getNewRating(oldLoserElo, oldWinnerElo, false);
+
+ final PraxiPlayer winningPraxiPlayer = PraxiPlayer.getByUuid(winningPlayer.getUniqueId());
+ final PraxiPlayer losingPraxiPlayer = PraxiPlayer.getByUuid(losingPlayer.getUniqueId());
+
+ if (winningPraxiPlayer.isLoaded()) {
+ winningPraxiPlayer.getStatistics().getLadderStatistics(this.getLadder()).setElo(newWinnerElo);
+ }
+
+ if (losingPraxiPlayer.isLoaded()) {
+ losingPraxiPlayer.getStatistics().getLadderStatistics(this.getLadder()).setElo(newLoserElo);
+ }
+
+ int winnerEloChange = newWinnerElo - oldWinnerElo;
+ int loserEloChange = oldLoserElo - newLoserElo;
+
+ components.add(new ChatComponentBuilder("")
+ .parse("&dELO Changes: &a" + winningPlayer.getName() + " +" + winnerEloChange + " (" +
+ newWinnerElo + ") &c" + losingPlayer.getName() + " -" + loserEloChange + " (" + newLoserElo +
+ ")")
+ .create());
+ }
+
+ components.add(0, new ChatComponentBuilder("").parse(Style.getBorderLine()).create());
+ components.add(new ChatComponentBuilder("").parse(Style.getBorderLine()).create());
+
+ for (Player player : new Player[]{ winningPlayer, losingPlayer }) {
+ components.forEach(player::sendMessage);
+ }
+
+ for (Player player : this.getSpectators()) {
+ components.forEach(player::sendMessage);
+ }
+ }
+
+ @Override
+ public boolean canEnd() {
+ if (this.getLadder().isSumo()) {
+ return this.playerA.isDisconnected()
+ || this.playerB.isDisconnected()
+ || (this.isRanked() ? (this.playerARoundWins == 3 || this.playerBRoundWins == 3)
+ : (this.playerARoundWins == 1 || this.playerBRoundWins == 1));
+ } else {
+ return !this.playerA.isAlive() || !this.playerB.isAlive();
+ }
+ }
+
+ @Override
+ public Player getWinningPlayer() {
+ if (this.playerA.isDisconnected() || !this.playerA.isAlive()) {
+ return this.playerB.toPlayer();
+ } else {
+ return this.playerA.toPlayer();
+ }
+ }
+
+ @Override
+ public MatchTeam getWinningTeam() {
+ throw new UnsupportedOperationException("Cannot get winning team from a SoloMatch");
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayerA() {
+ return this.playerA;
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayerB() {
+ return this.playerB;
+ }
+
+ @Override
+ public List getMatchPlayers() {
+ throw new UnsupportedOperationException("Cannot get match players from a SoloMatch");
+ }
+
+ @Override
+ public Player getPlayerA() {
+ return this.playerA.toPlayer();
+ }
+
+ @Override
+ public Player getPlayerB() {
+ return this.playerB.toPlayer();
+ }
+
+ @Override
+ public List getPlayers() {
+ final List players = new ArrayList<>();
+
+ final Player playerA = this.playerA.toPlayer();
+ final Player playerB = this.playerB.toPlayer();
+
+ if (playerA != null) {
+ players.add(playerA);
+ }
+
+ if (playerB != null) {
+ players.add(playerB);
+ }
+
+ return players;
+ }
+
+ @Override
+ public MatchTeam getTeamA() {
+ throw new UnsupportedOperationException("Cannot get team from a SoloMatch");
+ }
+
+ @Override
+ public MatchTeam getTeamB() {
+ throw new UnsupportedOperationException("Cannot get team from a SoloMatch");
+ }
+
+ @Override
+ public MatchTeam getTeam(MatchPlayer matchPlayer) {
+ throw new UnsupportedOperationException("Cannot get team from a SoloMatch");
+ }
+
+ @Override
+ public MatchTeam getTeam(Player player) {
+ throw new UnsupportedOperationException("Cannot get team from a SoloMatch");
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayer(Player player) {
+ if (this.playerA.getUuid().equals(player.getUniqueId())) {
+ return this.playerA;
+ } else if (this.playerB.getUuid().equals(player.getUniqueId())) {
+ return this.playerB;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getOpponentsLeft(Player player) {
+ throw new UnsupportedOperationException("Cannot get opponents left from a SoloMatch");
+ }
+
+ @Override
+ public MatchTeam getOpponentTeam(MatchTeam team) {
+ throw new UnsupportedOperationException("Cannot get opponent team from a SoloMatch");
+ }
+
+ @Override
+ public MatchTeam getOpponentTeam(Player player) {
+ throw new UnsupportedOperationException("Cannot get opponent team from a SoloMatch");
+ }
+
+ @Override
+ public Player getOpponentPlayer(Player player) {
+ if (player == null) {
+ return null;
+ }
+
+ if (this.playerA.getUuid().equals(player.getUniqueId())) {
+ return this.playerB.toPlayer();
+ } else if (this.playerB.getUuid().equals(player.getUniqueId())) {
+ return this.playerA.toPlayer();
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public MatchPlayer getOpponentMatchPlayer(Player player) {
+ if (this.playerA.getUuid().equals(player.getUniqueId())) {
+ return this.playerB;
+ } else if (this.playerB.getUuid().equals(player.getUniqueId())) {
+ return this.playerA;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public int getTotalRoundWins() {
+ return this.playerARoundWins + this.playerBRoundWins;
+ }
+
+ @Override
+ public int getRoundWins(MatchPlayer matchPlayer) {
+ if (this.playerA.equals(matchPlayer)) {
+ return this.playerARoundWins;
+ } else if (this.playerB.equals(matchPlayer)) {
+ return this.playerBRoundWins;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int getRoundWins(MatchTeam matchTeam) {
+ throw new UnsupportedOperationException("Cannot get team round wins from SoloMatch");
+ }
+
+ @Override
+ public int getRoundsNeeded(MatchPlayer matchPlayer) {
+ if (this.playerA.equals(matchPlayer)) {
+ return 3 - this.playerARoundWins;
+ } else if (this.playerB.equals(matchPlayer)) {
+ return 3 - this.playerBRoundWins;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int getRoundsNeeded(MatchTeam matchTeam) {
+ throw new UnsupportedOperationException("Cannot get team round wins from SoloMatch");
+ }
+
+ @Override
+ public void onDeath(Player player, Player killer) {
+ MatchPlayer roundLoser = this.getMatchPlayer(player);
+ MatchPlayer roundWinner = this.getOpponentMatchPlayer(player);
+
+ this.getSnapshots().add(new MatchSnapshot(roundLoser, roundWinner));
+
+ PlayerUtil.reset(player);
+
+ if (this.getLadder().isSumo()) {
+ if (this.playerA.getUuid().equals(player.getUniqueId())) {
+ this.playerBRoundWins++;
+ } else {
+ this.playerARoundWins++;
+ }
+
+ if (this.canEnd()) {
+ final String broadcast =
+ Style.PINK + roundWinner.getName() + Style.YELLOW + " has " + Style.GREEN + "won" +
+ Style.YELLOW + " the match.";
+
+ this.setState(MatchState.ENDING);
+ this.broadcastMessage(broadcast);
+ this.getOpponentPlayer(player).hidePlayer(player);
+ this.getSpectators().forEach(other -> other.hidePlayer(player));
+ } else {
+ final String broadcast =
+ Style.PINK + roundWinner.getName() + Style.YELLOW + " has " + Style.GREEN + "won" +
+ Style.YELLOW + " the round, they need " + Style.GOLD + this.getRoundsNeeded(roundWinner) +
+ Style.YELLOW + " more to win.";
+
+ this.broadcastMessage(broadcast);
+ this.handleStart();
+ }
+ }
+ }
+
+ @Override
+ public void onRespawn(Player player) {
+ if (this.getLadder().isSumo() && !this.isEnding()) {
+ player.teleport(this.getArena().getSpawn1());
+ this.getOpponentPlayer(player).teleport(this.getArena().getSpawn2());
+ } else {
+ player.teleport(player.getLocation().clone().add(0, 3, 0));
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/match/impl/TeamMatch.java b/plugin/src/main/java/me/joeleoli/praxi/match/impl/TeamMatch.java
new file mode 100644
index 0000000..26a70a7
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/match/impl/TeamMatch.java
@@ -0,0 +1,446 @@
+package me.joeleoli.praxi.match.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.Getter;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.MatchSnapshot;
+import me.joeleoli.praxi.match.MatchState;
+import me.joeleoli.praxi.match.MatchTeam;
+import net.md_5.bungee.api.ChatColor;
+import net.md_5.bungee.api.chat.BaseComponent;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.bukkit.entity.Player;
+
+@Getter
+public class TeamMatch extends Match {
+
+ private MatchTeam teamA;
+ private MatchTeam teamB;
+ private int teamARoundWins = 0;
+ private int teamBRoundWins = 0;
+
+ public TeamMatch(MatchTeam teamA, MatchTeam teamB, Ladder ladder, Arena arena) {
+ super(null, ladder, arena, false);
+
+ this.teamA = teamA;
+ this.teamB = teamB;
+ }
+
+ @Override
+ public boolean isDuel() {
+ return false;
+ }
+
+ @Override
+ public boolean isSoloMatch() {
+ return false;
+ }
+
+ @Override
+ public boolean isTeamMatch() {
+ return true;
+ }
+
+ @Override
+ public void onStart() {
+ }
+
+ @Override
+ public void onEnd() {
+ final MatchTeam winningTeam = this.getWinningTeam();
+ final MatchTeam losingTeam = this.getOpponentTeam(winningTeam);
+ final ChatComponentBuilder winnerInventories = new ChatComponentBuilder("");
+ final ChatComponentBuilder loserInventories = new ChatComponentBuilder("");
+
+ winnerInventories
+ .append("Winners: ")
+ .color(ChatColor.GREEN);
+ loserInventories
+ .append("Losers: ")
+ .color(ChatColor.RED);
+
+ for (MatchPlayer matchPlayer : winningTeam.getTeamPlayers()) {
+ final HoverEvent hover = new HoverEvent(HoverEvent.Action.SHOW_TEXT, HOVER_TEXT);
+ final ClickEvent click =
+ new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/viewinv " + matchPlayer.getUuid().toString());
+
+ winnerInventories
+ .append(matchPlayer.getName())
+ .color(ChatColor.YELLOW);
+ winnerInventories
+ .setCurrentHoverEvent(hover)
+ .setCurrentClickEvent(click)
+ .append(", ")
+ .color(ChatColor.YELLOW);
+ }
+
+ for (MatchPlayer matchPlayer : losingTeam.getTeamPlayers()) {
+ final HoverEvent hover = new HoverEvent(HoverEvent.Action.SHOW_TEXT, HOVER_TEXT);
+ final ClickEvent click =
+ new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/viewinv " + matchPlayer.getUuid().toString());
+
+ loserInventories
+ .append(matchPlayer.getName())
+ .color(ChatColor.YELLOW);
+ loserInventories
+ .setCurrentHoverEvent(hover)
+ .setCurrentClickEvent(click)
+ .append(", ")
+ .color(ChatColor.YELLOW);
+ }
+
+ winnerInventories.getCurrent().setText(winnerInventories.getCurrent().getText().substring(
+ 0,
+ winnerInventories.getCurrent().getText().length() - 2
+ ));
+ loserInventories.getCurrent().setText(loserInventories.getCurrent().getText().substring(
+ 0,
+ loserInventories.getCurrent().getText().length() - 2
+ ));
+
+ final List components = new ArrayList<>();
+
+ components.add(new ChatComponentBuilder("").parse(Style.getBorderLine()).create());
+ components.add(new ChatComponentBuilder("").parse("&dPost-Match Inventories &7(click name to view)").create());
+ components.add(winnerInventories.create());
+ components.add(loserInventories.create());
+ components.add(new ChatComponentBuilder("").parse(Style.getBorderLine()).create());
+
+ for (Player player : this.getPlayers()) {
+ components.forEach(player::sendMessage);
+ }
+
+ for (Player player : this.getSpectators()) {
+ components.forEach(player::sendMessage);
+ }
+ }
+
+ @Override
+ public boolean canEnd() {
+ if (this.getLadder().isSumo()) {
+ return (this.teamA.getDeadCount() + this.teamA.getDisconnectedCount()) >= this.teamA.getTeamPlayers().size()
+ ||
+ (this.teamB.getDeadCount() + this.teamB.getDisconnectedCount()) >= this.teamB.getTeamPlayers().size()
+ || this.teamARoundWins == 3
+ || this.teamBRoundWins == 3;
+ } else {
+ return this.teamA.getAliveTeamPlayers().isEmpty() || this.teamB.getAliveTeamPlayers().isEmpty();
+ }
+ }
+
+ @Override
+ public Player getWinningPlayer() {
+ throw new UnsupportedOperationException("Cannot get solo winning player from a TeamMatch");
+ }
+
+ @Override
+ public MatchTeam getWinningTeam() {
+ if (this.getLadder().isSumo()) {
+ if (this.teamA.getDisconnectedCount() == this.teamA.getTeamPlayers().size()) {
+ return this.teamB;
+ } else if (this.teamB.getDisconnectedCount() == this.teamB.getTeamPlayers().size()) {
+ return this.teamA;
+ }
+
+ return this.teamARoundWins == 3 ? this.teamA : this.teamB;
+ } else {
+ if (this.teamA.getAliveTeamPlayers().isEmpty()) {
+ return this.teamB;
+ } else if (this.teamB.getAliveTeamPlayers().isEmpty()) {
+ return this.teamA;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayerA() {
+ throw new UnsupportedOperationException("Cannot get solo match player from a TeamMatch");
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayerB() {
+ throw new UnsupportedOperationException("Cannot get solo match player from a TeamMatch");
+ }
+
+ @Override
+ public List getMatchPlayers() {
+ List matchPlayers = new ArrayList<>();
+
+ matchPlayers.addAll(this.teamA.getTeamPlayers());
+ matchPlayers.addAll(this.teamB.getTeamPlayers());
+
+ return matchPlayers;
+ }
+
+ @Override
+ public Player getPlayerA() {
+ throw new UnsupportedOperationException("Cannot get solo player from a TeamMatch");
+ }
+
+ @Override
+ public Player getPlayerB() {
+ throw new UnsupportedOperationException("Cannot get solo player from a TeamMatch");
+ }
+
+ @Override
+ public List getPlayers() {
+ List players = new ArrayList<>();
+
+ this.teamA.getTeamPlayers().forEach(matchPlayer -> {
+ Player player = matchPlayer.toPlayer();
+
+ if (player != null) {
+ players.add(player);
+ }
+ });
+
+ this.teamB.getTeamPlayers().forEach(matchPlayer -> {
+ Player player = matchPlayer.toPlayer();
+
+ if (player != null) {
+ players.add(player);
+ }
+ });
+
+ return players;
+ }
+
+ @Override
+ public MatchTeam getTeamA() {
+ return this.teamA;
+ }
+
+ @Override
+ public MatchTeam getTeamB() {
+ return this.teamB;
+ }
+
+ @Override
+ public MatchTeam getTeam(MatchPlayer matchPlayer) {
+ for (MatchPlayer teamMatchPlayer : this.teamA.getTeamPlayers()) {
+ if (teamMatchPlayer.getUuid().equals(matchPlayer.getUuid())) {
+ return this.teamA;
+ }
+ }
+
+ for (MatchPlayer teamMatchPlayer : this.teamB.getTeamPlayers()) {
+ if (teamMatchPlayer.getUuid().equals(matchPlayer.getUuid())) {
+ return this.teamB;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public MatchTeam getTeam(Player player) {
+ for (MatchPlayer teamMatchPlayer : this.teamA.getTeamPlayers()) {
+ if (teamMatchPlayer.getUuid().equals(player.getUniqueId())) {
+ return this.teamA;
+ }
+ }
+
+ for (MatchPlayer teamMatchPlayer : this.teamB.getTeamPlayers()) {
+ if (teamMatchPlayer.getUuid().equals(player.getUniqueId())) {
+ return this.teamB;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public MatchPlayer getMatchPlayer(Player player) {
+ for (MatchPlayer matchPlayer : this.teamA.getTeamPlayers()) {
+ if (matchPlayer.getUuid().equals(player.getUniqueId())) {
+ return matchPlayer;
+ }
+ }
+
+ for (MatchPlayer matchPlayer : this.teamB.getTeamPlayers()) {
+ if (matchPlayer.getUuid().equals(player.getUniqueId())) {
+ return matchPlayer;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public int getOpponentsLeft(Player player) {
+ if (this.teamA.containsPlayer(player)) {
+ return this.teamB.getAliveCount() - this.teamB.getDisconnectedCount();
+ } else if (this.teamB.containsPlayer(player)) {
+ return this.teamA.getAliveCount() - this.teamA.getDisconnectedCount();
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public MatchTeam getOpponentTeam(MatchTeam team) {
+ if (this.teamA.equals(team)) {
+ return this.teamB;
+ } else if (this.teamB.equals(team)) {
+ return this.teamA;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public MatchTeam getOpponentTeam(Player player) {
+ if (this.teamA.containsPlayer(player)) {
+ return this.teamB;
+ } else if (this.teamB.containsPlayer(player)) {
+ return this.teamA;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Player getOpponentPlayer(Player player) {
+ throw new UnsupportedOperationException("Cannot get solo opponent player from TeamMatch");
+ }
+
+ @Override
+ public MatchPlayer getOpponentMatchPlayer(Player player) {
+ throw new UnsupportedOperationException("Cannot get solo opponent match player from TeamMatch");
+ }
+
+ @Override
+ public int getTotalRoundWins() {
+ return this.teamARoundWins + this.teamBRoundWins;
+ }
+
+ @Override
+ public int getRoundWins(MatchPlayer matchPlayer) {
+ throw new UnsupportedOperationException("Cannot get solo round wins from TeamMatch");
+ }
+
+ @Override
+ public int getRoundWins(MatchTeam matchTeam) {
+ if (this.teamA.equals(matchTeam)) {
+ return this.teamARoundWins;
+ } else if (this.teamB.equals(matchTeam)) {
+ return this.teamBRoundWins;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int getRoundsNeeded(MatchPlayer matchPlayer) {
+ throw new UnsupportedOperationException("Cannot get solo rounds needed from TeamMatch");
+ }
+
+ @Override
+ public int getRoundsNeeded(MatchTeam matchTeam) {
+ if (this.teamA.equals(matchTeam)) {
+ return 3 - this.teamARoundWins;
+ } else if (this.teamB.equals(matchTeam)) {
+ return 3 - this.teamBRoundWins;
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public void onDeath(Player player, Player killer) {
+ // TODO: request teams messages directly then request global messages to spectators
+ // MatchTeam roundLoser = this.getOpponentTeam(player);
+
+ this.getSnapshots().add(new MatchSnapshot(this.getMatchPlayer(player)));
+
+ PlayerUtil.reset(player);
+
+ this.getPlayers().forEach(matchPlayer -> {
+ matchPlayer.hidePlayer(player);
+ });
+
+ if (this.getLadder().isSumo()) {
+ final MatchTeam deadTeam = this.getTeam(player);
+ final MatchTeam roundWinner = this.getOpponentTeam(deadTeam);
+ final int dead = deadTeam.getDisconnectedCount() + deadTeam.getDeadCount();
+
+ if (dead == deadTeam.getTeamPlayers().size()) {
+ if (this.teamA.equals(roundWinner)) {
+ this.teamARoundWins++;
+ } else {
+ this.teamBRoundWins++;
+ }
+
+ if (this.canEnd()) {
+ this.setState(MatchState.ENDING);
+ this.getPlayers().forEach(other -> other.hidePlayer(player));
+ this.getSpectators().forEach(other -> other.hidePlayer(player));
+ } else {
+ final String broadcast =
+ roundWinner.getLeader().getDisplayName() + Style.YELLOW + "'s team has " + Style.GREEN +
+ "won" + Style.YELLOW + " the round, they need " + Style.GOLD +
+ this.getRoundsNeeded(roundWinner) + Style.YELLOW + " more to win.";
+
+ this.broadcastMessage(broadcast);
+ this.handleStart();
+ }
+ } else {
+ for (Player other : this.getPlayers()) {
+ other.hidePlayer(player);
+ }
+
+ player.setAllowFlight(true);
+ player.setFlying(true);
+ player.updateInventory();
+ }
+ } else {
+ if (!this.canEnd()) {
+ player.setAllowFlight(true);
+ player.setFlying(true);
+ player.updateInventory();
+ }
+ }
+ }
+
+ @Override
+ public void onRespawn(Player player) {
+ if (this.getLadder().isSumo() && !this.isEnding()) {
+ for (MatchPlayer matchPlayer : this.teamA.getTeamPlayers()) {
+ if (matchPlayer.isDisconnected()) {
+ continue;
+ }
+
+ final Player toPlayer = matchPlayer.toPlayer();
+
+ if (toPlayer != null && toPlayer.isOnline()) {
+ toPlayer.teleport(this.getArena().getSpawn1());
+ }
+ }
+
+ for (MatchPlayer matchPlayer : this.teamB.getTeamPlayers()) {
+ if (matchPlayer.isDisconnected()) {
+ continue;
+ }
+
+ final Player toPlayer = matchPlayer.toPlayer();
+
+ if (toPlayer != null && toPlayer.isOnline()) {
+ toPlayer.teleport(this.getArena().getSpawn2());
+ }
+ }
+ } else {
+ player.teleport(player.getLocation().clone().add(0, 3, 0));
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/mongo/PraxiMongo.java b/plugin/src/main/java/me/joeleoli/praxi/mongo/PraxiMongo.java
new file mode 100644
index 0000000..67d5dd0
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/mongo/PraxiMongo.java
@@ -0,0 +1,66 @@
+package me.joeleoli.praxi.mongo;
+
+import com.mongodb.MongoClient;
+import com.mongodb.MongoCredential;
+import com.mongodb.ServerAddress;
+import com.mongodb.client.MongoCollection;
+import com.mongodb.client.MongoDatabase;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.ReplaceOptions;
+import java.util.Collections;
+import java.util.UUID;
+import me.joeleoli.nucleus.config.ConfigCursor;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bson.Document;
+
+public class PraxiMongo {
+
+ private MongoClient client;
+ private MongoDatabase database;
+ private MongoCollection players;
+ private MongoCollection matches;
+
+ public PraxiMongo() {
+ ConfigCursor cursor = new ConfigCursor(Praxi.getInstance().getMainConfig(), "mongo");
+
+ if (!cursor.exists("host")
+ || !cursor.exists("port")
+ || !cursor.exists("database")
+ || !cursor.exists("authentication.enabled")
+ || !cursor.exists("authentication.username")
+ || !cursor.exists("authentication.password")
+ || !cursor.exists("authentication.database")) {
+ throw new RuntimeException("Missing configuration option");
+ }
+
+ if (cursor.getBoolean("authentication.enabled")) {
+ final MongoCredential credential = MongoCredential.createCredential(
+ cursor.getString("authentication.username"),
+ cursor.getString("authentication.database"),
+ cursor.getString("authentication.password").toCharArray()
+ );
+
+ this.client = new MongoClient(new ServerAddress(cursor.getString("host"), cursor.getInt("port")),
+ Collections.singletonList(credential)
+ );
+ } else {
+ this.client = new MongoClient(new ServerAddress(cursor.getString("host"), cursor.getInt("port")));
+ }
+
+ this.database = this.client.getDatabase("praxi");
+ this.players = this.database.getCollection("players");
+ this.matches = this.database.getCollection("matches");
+ }
+
+ public Document getPlayer(UUID uuid) {
+ return this.players.find(Filters.eq("uuid", uuid.toString())).first();
+ }
+
+ public void replacePlayer(PraxiPlayer player, Document document) {
+ this.players.replaceOne(Filters.eq("uuid", player.getUuid().toString()), document,
+ new ReplaceOptions().upsert(true)
+ );
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/Party.java b/plugin/src/main/java/me/joeleoli/praxi/party/Party.java
new file mode 100644
index 0000000..f90c766
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/Party.java
@@ -0,0 +1,154 @@
+package me.joeleoli.praxi.party;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.team.Team;
+import me.joeleoli.nucleus.team.TeamPlayer;
+import me.joeleoli.nucleus.util.ObjectUtil;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.bukkit.entity.Player;
+
+@Getter
+public class Party extends Team {
+
+ @Getter
+ private static List parties = new ArrayList<>();
+
+ private PartyState state = PartyState.CLOSED;
+ private Map invited;
+ @Setter
+ private PartyEvent selectedEvent;
+
+ public Party(Player player) {
+ super(new TeamPlayer(player.getUniqueId(), player.getName()));
+
+ this.invited = new HashMap<>();
+
+ parties.add(this);
+ }
+
+ public void setState(PartyState state) {
+ this.state = state;
+
+ this.broadcast(Style.YELLOW + "The party state has been changed to: " + Style.RESET + this.state.name());
+ }
+
+ public boolean canInvite(Player player) {
+ for (UUID uuid : this.invited.keySet()) {
+ if (uuid.equals(player.getUniqueId())) {
+ if (System.currentTimeMillis() - this.invited.get(uuid) >= 30_000) {
+ this.invited.remove(uuid);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public boolean isInvited(Player player) {
+ for (UUID uuid : this.invited.keySet()) {
+ if (uuid.equals(player.getUniqueId())) {
+ if (System.currentTimeMillis() - this.invited.get(uuid) >= 30_000) {
+ this.invited.remove(uuid);
+ return false;
+ }
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public void invite(Player target) {
+ this.invited.put(target.getUniqueId(), System.currentTimeMillis());
+
+ final HoverEvent hoverEvent = new HoverEvent(
+ HoverEvent.Action.SHOW_TEXT,
+ new ChatComponentBuilder(Style.YELLOW + "Click to join the party.").create()
+ );
+ final ClickEvent clickEvent =
+ new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/party join " + this.getLeader().getName());
+
+ this.broadcast(Style.RESET + target.getDisplayName() + " " + Style.YELLOW + "has been invited to the party.");
+
+ target.sendMessage(
+ Style.YELLOW + "You have been invited to join " + Style.RESET + this.getLeader().getDisplayName() +
+ Style.YELLOW + "'s party.");
+ target.sendMessage(new ChatComponentBuilder("").parse(Style.GOLD + "Click here to join the party.")
+ .attachToEachPart(clickEvent).attachToEachPart(hoverEvent)
+ .create());
+ }
+
+ public void join(Player player) {
+ this.getTeamPlayers().add(new TeamPlayer(player.getUniqueId(), player.getName()));
+ this.invited.keySet().removeIf(uuid -> uuid.equals(player.getUniqueId()));
+ this.broadcast(Style.RESET + player.getDisplayName() + Style.YELLOW + " has joined the party.");
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setParty(this);
+ praxiPlayer.loadHotbar();
+ }
+
+ public void leave(Player player, boolean kick) {
+ this.broadcast(
+ Style.RESET + player.getDisplayName() + Style.YELLOW + " has " + (kick ? "been kicked" : "left") +
+ " the party.");
+ this.getTeamPlayers().removeIf(playerInfo -> playerInfo.getUuid().equals(player.getUniqueId()));
+
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setParty(null);
+ praxiPlayer.loadHotbar();
+ }
+
+ public void disband() {
+ parties.remove(this);
+
+ this.broadcast(Style.YELLOW + "The party has been disbanded.");
+
+ this.getPlayers().forEach(player -> {
+ PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.setParty(null);
+
+ if (praxiPlayer.isInLobby()) {
+ praxiPlayer.loadHotbar();
+ }
+ });
+ }
+
+ public void sendInformation(Player player) {
+ StringBuilder builder = new StringBuilder();
+
+ for (Player member : this.getPlayers()) {
+ builder.append(member.getName());
+ builder.append(", ");
+ }
+
+ final String[] lines = new String[]{
+ Style.getBorderLine(),
+ Style.GOLD + "Party of " + this.getLeader().getName(),
+ Style.YELLOW + "State: " + Style.GRAY + ObjectUtil.toReadable(this.state),
+ Style.YELLOW + "Members: " + Style.GRAY +
+ builder.toString().substring(0, builder.toString().length() - 2),
+ Style.getBorderLine()
+ };
+
+ player.sendMessage(lines);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/PartyEvent.java b/plugin/src/main/java/me/joeleoli/praxi/party/PartyEvent.java
new file mode 100644
index 0000000..d1390a3
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/PartyEvent.java
@@ -0,0 +1,15 @@
+package me.joeleoli.praxi.party;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@AllArgsConstructor
+@Getter
+public enum PartyEvent {
+
+ FFA("FFA"),
+ SPLIT("Split");
+
+ private String name;
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/PartyState.java b/plugin/src/main/java/me/joeleoli/praxi/party/PartyState.java
new file mode 100644
index 0000000..6de19e9
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/PartyState.java
@@ -0,0 +1,6 @@
+package me.joeleoli.praxi.party;
+
+public enum PartyState {
+ OPEN,
+ CLOSED
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/gui/OtherPartiesMenu.java b/plugin/src/main/java/me/joeleoli/praxi/party/gui/OtherPartiesMenu.java
new file mode 100644
index 0000000..f2d4bc6
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/gui/OtherPartiesMenu.java
@@ -0,0 +1,30 @@
+package me.joeleoli.praxi.party.gui;
+
+import java.util.HashMap;
+import java.util.Map;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.pagination.PaginatedMenu;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.party.Party;
+import me.joeleoli.praxi.party.gui.button.PartyDisplayButton;
+import org.bukkit.entity.Player;
+
+public class OtherPartiesMenu extends PaginatedMenu {
+
+ @Override
+ public String getPrePaginatedTitle(Player player) {
+ return Style.GOLD + "Other Parties";
+ }
+
+ @Override
+ public Map getAllPagesButtons(Player player) {
+ Map buttons = new HashMap<>();
+
+ Party.getParties().forEach(party -> {
+ buttons.put(buttons.size(), new PartyDisplayButton(party));
+ });
+
+ return buttons;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectEventMenu.java b/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectEventMenu.java
new file mode 100644
index 0000000..1759ef3
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectEventMenu.java
@@ -0,0 +1,68 @@
+package me.joeleoli.praxi.party.gui;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.party.PartyEvent;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class PartyEventSelectEventMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.BLUE + Style.BOLD + "Select an event";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final Map buttons = new HashMap<>();
+
+ buttons.put(3, new SelectEventButton(PartyEvent.FFA));
+ buttons.put(5, new SelectEventButton(PartyEvent.SPLIT));
+
+ return buttons;
+ }
+
+ @AllArgsConstructor
+ private class SelectEventButton extends Button {
+
+ private PartyEvent partyEvent;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(this.partyEvent == PartyEvent.FFA ? Material.QUARTZ : Material.REDSTONE)
+ .name(Style.GREEN + Style.BOLD + this.partyEvent.getName())
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click here to select " + Style.GREEN + Style.BOLD +
+ this.partyEvent.getName() + Style.YELLOW + "."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarSlot) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You are not in a party.");
+ return;
+ }
+
+ praxiPlayer.getParty().setSelectedEvent(this.partyEvent);
+
+ new PartyEventSelectLadderMenu().openMenu(player);
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectLadderMenu.java b/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectLadderMenu.java
new file mode 100644
index 0000000..301e747
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/gui/PartyEventSelectLadderMenu.java
@@ -0,0 +1,151 @@
+package me.joeleoli.praxi.party.gui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.MatchTeam;
+import me.joeleoli.praxi.match.impl.TeamMatch;
+import me.joeleoli.praxi.party.Party;
+import me.joeleoli.praxi.party.PartyEvent;
+import me.joeleoli.praxi.player.PlayerState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class PartyEventSelectLadderMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.GOLD + Style.BOLD + "Select a ladder";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final Map buttons = new HashMap<>();
+
+ for (Ladder ladder : Ladder.getLadders()) {
+ if (ladder.isEnabled()) {
+ buttons.put(buttons.size(), new SelectLadderButton(ladder));
+ }
+ }
+
+ return buttons;
+ }
+
+ @Override
+ public void onClose(Player player) {
+ if (!this.isClosedByMenu()) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() != null) {
+ praxiPlayer.getParty().setSelectedEvent(null);
+ }
+ }
+ }
+
+ @AllArgsConstructor
+ private class SelectLadderButton extends Button {
+
+ private Ladder ladder;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ return new ItemBuilder(this.ladder.getDisplayIcon())
+ .name(Style.PINK + Style.BOLD + this.ladder.getName())
+ .lore(Arrays.asList(
+ "",
+ Style.YELLOW + "Click here to select " + Style.PINK + Style.BOLD +
+ this.ladder.getName() + Style.YELLOW + "."
+ ))
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hbSlot) {
+ Menu.currentlyOpenedMenus.get(player.getName()).setClosedByMenu(true);
+
+ player.closeInventory();
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer.getParty() == null) {
+ player.sendMessage(Style.RED + "You are not in a party.");
+ return;
+ }
+
+ if (praxiPlayer.getParty().getSelectedEvent() == null) {
+ return;
+ }
+
+ if (praxiPlayer.getParty().getTeamPlayers().size() <= 1) {
+ player.sendMessage(Style.RED + "You do not have enough players in your party to start an event.");
+ return;
+ }
+
+ Party party = praxiPlayer.getParty();
+ Arena arena = Arena.getRandom(this.ladder);
+
+ if (arena == null) {
+ player.sendMessage(Style.RED + "There are no available arenas.");
+ return;
+ }
+
+ arena.setActive(true);
+
+ Match match;
+
+ if (party.getSelectedEvent() == PartyEvent.FFA) {
+ player.sendMessage(Style.RED + "The FFA party event is currently disabled.");
+ return;
+ } else {
+ MatchTeam teamA = new MatchTeam(new MatchPlayer(party.getLeader().toPlayer()));
+ MatchTeam teamB = new MatchTeam(new MatchPlayer(party.getPlayers().get(1)));
+
+ final List players = new ArrayList<>();
+
+ players.addAll(party.getPlayers());
+
+ Collections.shuffle(players);
+
+ // Create match
+ match = new TeamMatch(teamA, teamB, this.ladder, arena);
+
+ for (Player other : players) {
+ final PraxiPlayer otherData = PraxiPlayer.getByUuid(other.getUniqueId());
+
+ otherData.setState(PlayerState.IN_MATCH);
+ otherData.setMatch(match);
+
+ if (teamA.getLeader().getUuid().equals(other.getUniqueId()) ||
+ teamB.getLeader().getUuid().equals(other.getUniqueId())) {
+ continue;
+ }
+
+ if (teamA.getTeamPlayers().size() > teamB.getTeamPlayers().size()) {
+ teamB.getTeamPlayers().add(new MatchPlayer(other));
+ } else {
+ teamA.getTeamPlayers().add(new MatchPlayer(other));
+ }
+ }
+ }
+
+ // Start match
+ match.handleStart();
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/party/gui/button/PartyDisplayButton.java b/plugin/src/main/java/me/joeleoli/praxi/party/gui/button/PartyDisplayButton.java
new file mode 100644
index 0000000..6412642
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/party/gui/button/PartyDisplayButton.java
@@ -0,0 +1,47 @@
+package me.joeleoli.praxi.party.gui.button;
+
+import java.util.ArrayList;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.team.TeamPlayer;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.party.Party;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+@AllArgsConstructor
+public class PartyDisplayButton extends Button {
+
+ private Party party;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ final List lore = new ArrayList<>();
+ int added = 0;
+
+ for (TeamPlayer teamPlayer : this.party.getTeamPlayers()) {
+ if (added >= 10) {
+ break;
+ }
+
+ lore.add(Style.GRAY + " - " + Style.RESET + teamPlayer.getDisplayName());
+
+ added++;
+ }
+
+ if (this.party.getTeamPlayers().size() != added) {
+ lore.add(Style.GRAY + " and " + (this.party.getTeamPlayers().size() - added) + " others...");
+ }
+
+ return new ItemBuilder(Material.SKULL_ITEM)
+ .amount(this.party.getTeamPlayers().size())
+ .durability(3)
+ .name(Style.GOLD + this.party.getLeader().getName() + "s Party")
+ .lore(lore)
+ .build();
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/KitEditor.java b/plugin/src/main/java/me/joeleoli/praxi/player/KitEditor.java
new file mode 100644
index 0000000..d83ba8c
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/KitEditor.java
@@ -0,0 +1,25 @@
+package me.joeleoli.praxi.player;
+
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.praxi.kit.NamedKit;
+import me.joeleoli.praxi.ladder.Ladder;
+
+@Setter
+public class KitEditor {
+
+ @Getter
+ private boolean active;
+ private boolean rename;
+ @Getter
+ private PlayerState previousState;
+ @Getter
+ private Ladder selectedLadder;
+ @Getter
+ private NamedKit selectedKit;
+
+ public boolean isRenaming() {
+ return this.active && this.rename && this.selectedKit != null;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/LadderStatistics.java b/plugin/src/main/java/me/joeleoli/praxi/player/LadderStatistics.java
new file mode 100644
index 0000000..bb168ba
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/LadderStatistics.java
@@ -0,0 +1,19 @@
+package me.joeleoli.praxi.player;
+
+import lombok.Data;
+
+@Data
+public class LadderStatistics {
+
+ private int elo = 1000;
+ private int won, lost;
+
+ public void incrementWon() {
+ this.won++;
+ }
+
+ public void incrementLost() {
+ this.lost++;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/PlayerHotbar.java b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerHotbar.java
new file mode 100644
index 0000000..c369d11
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerHotbar.java
@@ -0,0 +1,231 @@
+package me.joeleoli.praxi.player;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Predicate;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+
+public class PlayerHotbar {
+
+ @Getter
+ private static Map items = new HashMap<>();
+
+ // Utility class - cannot be instantiated
+ private PlayerHotbar() {
+ }
+
+ public static void init() {
+ items.put(
+ HotbarItem.QUEUE_JOIN_UNRANKED,
+ new ItemBuilder(Material.IRON_SWORD).name(Style.GRAY + Style.BOLD + "Unranked Queue")
+ .lore(Style.YELLOW + "Right-click to join an unranked queue.")
+ .build()
+ );
+ items.put(
+ HotbarItem.QUEUE_JOIN_RANKED,
+ new ItemBuilder(Material.DIAMOND_SWORD).name(Style.GREEN + Style.BOLD + "Ranked Queue")
+ .lore(Style.YELLOW + "Right-click to join a ranked queue.")
+ .build()
+ );
+ items.put(
+ HotbarItem.QUEUE_LEAVE,
+ new ItemBuilder(Material.INK_SACK).durability(1).name(Style.RED + Style.BOLD + "Leave Queue")
+ .lore(Style.YELLOW + "Right-click to leave your queue.").build()
+ );
+ items.put(
+ HotbarItem.PARTY_EVENTS,
+ new ItemBuilder(Material.DIAMOND_SWORD).name(Style.GREEN + Style.BOLD + "Party Events")
+ .lore(Style.YELLOW + "Right-click to start a party event.")
+ .build()
+ );
+ items.put(
+ HotbarItem.PARTY_CREATE,
+ new ItemBuilder(Material.NAME_TAG).name(Style.YELLOW + Style.BOLD + "Create Party")
+ .lore(Style.YELLOW + "Right-click to create a party.").build()
+ );
+ items.put(
+ HotbarItem.PARTY_DISBAND,
+ new ItemBuilder(Material.INK_SACK).durability(1).name(Style.RED + Style.BOLD + "Disband Party")
+ .lore(Style.YELLOW + "Right-click to disband your party.").build()
+ );
+ items.put(
+ HotbarItem.PARTY_LEAVE,
+ new ItemBuilder(Material.INK_SACK).durability(1).name(Style.RED + Style.BOLD + "Leave Party")
+ .lore(Style.YELLOW + "Right-click to leave your party.").build()
+ );
+ items.put(
+ HotbarItem.PARTY_INFORMATION,
+ new ItemBuilder(Material.SKULL_ITEM).durability(3).name(Style.YELLOW + Style.BOLD + "Party Information")
+ .lore(Style.YELLOW +
+ "Right-click to show your party's information.").build()
+ );
+ items.put(
+ HotbarItem.OTHER_PARTIES,
+ new ItemBuilder(Material.CHEST).name(Style.BLUE + Style.BOLD + "Other Parties")
+ .lore(Style.YELLOW + "Right-click to show other parties.").build()
+ );
+ items.put(HotbarItem.SETTINGS, new ItemBuilder(Material.WATCH).name(Style.PINK + Style.BOLD + "Settings")
+ .lore(Style.YELLOW +
+ "Right-click to open your settings.")
+ .build());
+ items.put(HotbarItem.KIT_EDITOR, new ItemBuilder(Material.BOOK).name(Style.RED + Style.BOLD + "Kit Editor")
+ .lore(Style.YELLOW +
+ "Right-click to open the kit editor.")
+ .build());
+ items.put(
+ HotbarItem.SPECTATE_STOP,
+ new ItemBuilder(Material.INK_SACK).durability(1).name(Style.RED + Style.BOLD + "Stop Spectating")
+ .lore(Style.YELLOW + "Right-click to stop spectating.").build()
+ );
+ items.put(
+ HotbarItem.VIEW_INVENTORY,
+ new ItemBuilder(Material.BOOK).name(Style.GOLD + Style.BOLD + "View Inventory")
+ .lore(Style.YELLOW + "Right-click a player to view their inventory.")
+ .build()
+ );
+ items.put(
+ HotbarItem.EVENT_JOIN,
+ new ItemBuilder(Material.NETHER_STAR).name(Style.AQUA + Style.BOLD + "Join Event")
+ .lore(Style.YELLOW + "Right-click to join the event.").build()
+ );
+ items.put(
+ HotbarItem.EVENT_LEAVE,
+ new ItemBuilder(Material.NETHER_STAR).name(Style.RED + Style.BOLD + "Leave Event")
+ .lore(Style.YELLOW + "Right-click to leave the event.").build()
+ );
+ items.put(
+ HotbarItem.REMATCH_REQUEST,
+ new ItemBuilder(Material.EMERALD).name(Style.DARK_GREEN + Style.BOLD + "Request Rematch")
+ .lore(Style.YELLOW + "Right-click to request a rematch.").build()
+ );
+ items.put(
+ HotbarItem.REMATCH_ACCEPT,
+ new ItemBuilder(Material.DIAMOND).name(Style.AQUA + Style.BOLD + "Accept Rematch")
+ .lore(Style.YELLOW + "Right-click to accept a rematch.").build()
+ );
+ }
+
+ public static ItemStack[] getLayout(HotbarLayout layout, PraxiPlayer praxiPlayer) {
+ final ItemStack[] toReturn = new ItemStack[9];
+
+ Arrays.fill(toReturn, null);
+
+ switch (layout) {
+ case LOBBY: {
+ if (praxiPlayer.getParty() == null) {
+ toReturn[0] = items.get(HotbarItem.QUEUE_JOIN_UNRANKED);
+ toReturn[1] = items.get(HotbarItem.QUEUE_JOIN_RANKED);
+
+ if (praxiPlayer.getRematchData() != null) {
+ if (praxiPlayer.getRematchData().isReceive()) {
+ toReturn[2] = items.get(HotbarItem.REMATCH_ACCEPT);
+ } else {
+ toReturn[2] = items.get(HotbarItem.REMATCH_REQUEST);
+ }
+
+ toReturn[4] = items.get(HotbarItem.PARTY_CREATE);
+
+ if (Praxi.getInstance().getEventManager().getActiveEvent() != null && Praxi.getInstance().getEventManager().getActiveEvent().isWaiting()) {
+ toReturn[6] = items.get(HotbarItem.EVENT_JOIN);
+ }
+ } else {
+ if (Praxi.getInstance().getEventManager().getActiveEvent() != null && Praxi.getInstance().getEventManager().getActiveEvent().isWaiting()) {
+ toReturn[3] = items.get(HotbarItem.EVENT_JOIN);
+ toReturn[5] = items.get(HotbarItem.PARTY_CREATE);
+ } else {
+ toReturn[4] = items.get(HotbarItem.PARTY_CREATE);
+ }
+ }
+
+ toReturn[7] = items.get(HotbarItem.SETTINGS);
+ toReturn[8] = items.get(HotbarItem.KIT_EDITOR);
+ } else {
+ if (praxiPlayer.getParty().isLeader(praxiPlayer.getUuid())) {
+ toReturn[0] = items.get(HotbarItem.PARTY_EVENTS);
+ toReturn[2] = items.get(HotbarItem.PARTY_INFORMATION);
+ toReturn[3] = items.get(HotbarItem.OTHER_PARTIES);
+ toReturn[5] = items.get(HotbarItem.PARTY_DISBAND);
+ toReturn[7] = items.get(HotbarItem.SETTINGS);
+ toReturn[8] = items.get(HotbarItem.KIT_EDITOR);
+ } else {
+ toReturn[0] = items.get(HotbarItem.PARTY_INFORMATION);
+ toReturn[2] = items.get(HotbarItem.OTHER_PARTIES);
+ toReturn[4] = items.get(HotbarItem.PARTY_LEAVE);
+ toReturn[7] = items.get(HotbarItem.SETTINGS);
+ toReturn[8] = items.get(HotbarItem.KIT_EDITOR);
+ }
+ }
+ }
+ break;
+ case QUEUE: {
+ toReturn[0] = items.get(HotbarItem.QUEUE_LEAVE);
+ toReturn[7] = items.get(HotbarItem.SETTINGS);
+ toReturn[8] = items.get(HotbarItem.KIT_EDITOR);
+ }
+ break;
+ case EVENT_SPECTATE: {
+ toReturn[0] = items.get(HotbarItem.EVENT_LEAVE);
+ toReturn[8] = items.get(HotbarItem.SETTINGS);
+ }
+ break;
+ case MATCH_SPECTATE: {
+ toReturn[0] = items.get(HotbarItem.SPECTATE_STOP);
+
+ if (!praxiPlayer.getMatch().isRanked()) {
+ toReturn[5] = items.get(HotbarItem.VIEW_INVENTORY);
+ }
+
+ toReturn[8] = items.get(HotbarItem.SETTINGS);
+ }
+ break;
+ }
+
+ return toReturn;
+ }
+
+ public static HotbarItem fromItemStack(ItemStack itemStack) {
+ for (Map.Entry entry : PlayerHotbar.getItems().entrySet()) {
+ if (entry.getValue() != null && entry.getValue().equals(itemStack)) {
+ return entry.getKey();
+ }
+ }
+
+ return null;
+ }
+
+ @AllArgsConstructor
+ public enum HotbarItem {
+ QUEUE_JOIN_RANKED,
+ QUEUE_JOIN_UNRANKED,
+ QUEUE_LEAVE,
+ PARTY_EVENTS,
+ PARTY_CREATE,
+ PARTY_DISBAND,
+ PARTY_LEAVE,
+ PARTY_INFORMATION,
+ OTHER_PARTIES,
+ SETTINGS,
+ KIT_EDITOR,
+ SPECTATE_STOP,
+ VIEW_INVENTORY,
+ EVENT_JOIN,
+ EVENT_LEAVE,
+ REMATCH_REQUEST,
+ REMATCH_ACCEPT
+ }
+
+ public enum HotbarLayout {
+ LOBBY,
+ QUEUE,
+ MATCH_SPECTATE,
+ EVENT_SPECTATE,
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/PlayerState.java b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerState.java
new file mode 100644
index 0000000..0454e47
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerState.java
@@ -0,0 +1,11 @@
+package me.joeleoli.praxi.player;
+
+public enum PlayerState {
+
+ IN_LOBBY,
+ IN_QUEUE,
+ IN_MATCH,
+ IN_EVENT,
+ SPECTATE_MATCH
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/PlayerStatistics.java b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerStatistics.java
new file mode 100644
index 0000000..07a1a1f
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/PlayerStatistics.java
@@ -0,0 +1,68 @@
+package me.joeleoli.praxi.player;
+
+import java.util.HashMap;
+import java.util.Map;
+import lombok.Getter;
+import me.joeleoli.praxi.ladder.Ladder;
+
+@Getter
+public class PlayerStatistics {
+
+ private Map ladders;
+
+ public PlayerStatistics() {
+ this.ladders = new HashMap<>();
+
+ for (Ladder ladder : Ladder.getLadders()) {
+ this.ladders.put(ladder.getName(), new LadderStatistics());
+ }
+ }
+
+ public int getElo(Ladder ladder) {
+ if (!this.ladders.containsKey(ladder.getName())) {
+ return 1000;
+ }
+
+ return this.ladders.get(ladder.getName()).getElo();
+ }
+
+ public LadderStatistics getLadderStatistics(Ladder ladder) {
+ LadderStatistics ladderStatistics = this.ladders.get(ladder.getName());
+
+ if (ladderStatistics == null) {
+ ladderStatistics = new LadderStatistics();
+ this.ladders.put(ladder.getName(), ladderStatistics);
+ }
+
+ return ladderStatistics;
+ }
+
+ public int getWins() {
+ int wins = 0;
+
+ for (LadderStatistics stats : this.ladders.values()) {
+ wins += stats.getWon();
+ }
+
+ return wins;
+ }
+
+ public double getWinRatio() {
+ int wins = 0;
+ int losses = 0;
+
+ for (LadderStatistics ladder : this.ladders.values()) {
+ wins += ladder.getWon();
+ losses += ladder.getLost();
+ }
+
+ if (losses == 0) {
+ return 100.0;
+ } else if (wins == 0 && losses > 0) {
+ return 0.0;
+ } else {
+ return (wins / (wins + losses)) * 100;
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/PracticeSetting.java b/plugin/src/main/java/me/joeleoli/praxi/player/PracticeSetting.java
new file mode 100644
index 0000000..fd60c06
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/PracticeSetting.java
@@ -0,0 +1,12 @@
+package me.joeleoli.praxi.player;
+
+import me.joeleoli.nucleus.player.DefinedSetting;
+
+public enum PracticeSetting implements DefinedSetting {
+
+ RECEIVE_DUEL_REQUESTS,
+ SHOW_SCOREBOARD,
+ ALLOW_SPECTATORS,
+ PING_FACTOR,
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/PraxiPlayer.java b/plugin/src/main/java/me/joeleoli/praxi/player/PraxiPlayer.java
new file mode 100644
index 0000000..443994a
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/PraxiPlayer.java
@@ -0,0 +1,394 @@
+package me.joeleoli.praxi.player;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import lombok.Getter;
+import lombok.Setter;
+import me.joeleoli.nucleus.cooldown.Cooldown;
+import me.joeleoli.nucleus.player.PlayerInfo;
+import me.joeleoli.nucleus.util.InventoryUtil;
+import me.joeleoli.nucleus.util.PlayerUtil;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.duel.DuelProcedure;
+import me.joeleoli.praxi.duel.DuelRequest;
+import me.joeleoli.praxi.events.Event;
+import me.joeleoli.praxi.kit.Kit;
+import me.joeleoli.praxi.kit.NamedKit;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.party.Party;
+import me.joeleoli.praxi.queue.QueuePlayer;
+import org.bson.Document;
+import org.bukkit.Bukkit;
+import org.bukkit.ChatColor;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+@Getter
+public class PraxiPlayer extends PlayerInfo {
+
+ @Getter
+ private static Map players = new HashMap<>();
+
+ @Setter
+ private PlayerState state;
+ private PlayerStatistics statistics = new PlayerStatistics();
+ private KitEditor kitEditor = new KitEditor();
+ private Map kits = new HashMap<>();
+ @Setter
+ private Party party;
+ @Setter
+ private Match match;
+ @Setter
+ private Event event;
+ @Setter
+ private QueuePlayer queuePlayer;
+ @Setter
+ private Cooldown enderpearlCooldown = new Cooldown(0);
+ private Map sentDuelRequests = new HashMap<>();
+ @Setter
+ private DuelProcedure duelProcedure;
+ @Setter
+ private RematchData rematchData;
+ private boolean loaded;
+
+ public PraxiPlayer(UUID uuid, String name) {
+ super(uuid, name);
+
+ this.state = PlayerState.IN_LOBBY;
+
+ for (Ladder ladder : Ladder.getLadders()) {
+ this.kits.put(ladder.getName(), new NamedKit[4]);
+ }
+ }
+
+ public static PraxiPlayer getByUuid(UUID uuid) {
+ PraxiPlayer praxiPlayer = players.get(uuid);
+
+ if (praxiPlayer == null) {
+ praxiPlayer = new PraxiPlayer(uuid, null);
+ }
+
+ return praxiPlayer;
+ }
+
+ public boolean canSendDuelRequest(Player player) {
+ if (!this.sentDuelRequests.containsKey(player.getUniqueId())) {
+ return true;
+ }
+
+ final DuelRequest request = this.sentDuelRequests.get(player.getUniqueId());
+
+ if (request.isExpired()) {
+ this.sentDuelRequests.remove(player.getUniqueId());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isPendingDuelRequest(Player player) {
+ if (!this.sentDuelRequests.containsKey(player.getUniqueId())) {
+ return false;
+ }
+
+ final DuelRequest request = this.sentDuelRequests.get(player.getUniqueId());
+
+ if (request.isExpired()) {
+ this.sentDuelRequests.remove(player.getUniqueId());
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public boolean isInLobby() {
+ return this.state == PlayerState.IN_LOBBY;
+ }
+
+ public boolean isInQueue() {
+ return this.state == PlayerState.IN_QUEUE && this.queuePlayer != null;
+ }
+
+ public boolean isInMatch() {
+ return (this.state == PlayerState.IN_MATCH) && this.match != null;
+ }
+
+ public boolean isSpectating() {
+ return this.state == PlayerState.SPECTATE_MATCH && this.match != null;
+ }
+
+ public boolean isInEvent() {
+ return this.state == PlayerState.IN_EVENT && this.event != null;
+ }
+
+ public boolean isBusy() {
+ return this.isInMatch() || this.isInQueue() || this.isInEvent() || this.isSpectating() ||
+ this.getParty() != null;
+ }
+
+ public void refreshHotbar() {
+ final Player player = this.toPlayer();
+
+ if (player == null) {
+ return;
+ }
+
+ if (this.isInLobby() && !this.kitEditor.isActive()) {
+ boolean update = false;
+
+ if (this.rematchData != null) {
+ final Player target = Bukkit.getPlayer(this.rematchData.getTarget());
+
+ if (System.currentTimeMillis() - this.rematchData.getTimestamp() >= 30_000) {
+ this.rematchData = null;
+ update = true;
+ } else if (target == null || !target.isOnline()) {
+ this.rematchData = null;
+ update = true;
+ } else {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (!(praxiPlayer.isInLobby() || praxiPlayer.isInQueue())) {
+ this.rematchData = null;
+ update = true;
+ } else if (praxiPlayer.getRematchData() == null) {
+ this.rematchData = null;
+ update = true;
+ } else if (!this.rematchData.getKey().equals(praxiPlayer.getRematchData().getKey())) {
+ this.rematchData = null;
+ update = true;
+ } else if (this.rematchData.isReceive()) {
+ final int requestSlot = player.getInventory().first(PlayerHotbar.getItems()
+ .get(PlayerHotbar.HotbarItem.REMATCH_REQUEST));
+
+ if (requestSlot != -1) {
+ update = true;
+ }
+ }
+ }
+ }
+
+ {
+ final Event activeEvent = Praxi.getInstance().getEventManager().getActiveEvent();
+ final int eventSlot =
+ player.getInventory().first(PlayerHotbar.getItems().get(PlayerHotbar.HotbarItem.EVENT_JOIN));
+
+ if (eventSlot == -1 && activeEvent != null && activeEvent.isWaiting()) {
+ update = true;
+ } else if (eventSlot != -1 && (activeEvent == null || !activeEvent.isWaiting())) {
+ update = true;
+ }
+ }
+
+ if (update) {
+ TaskUtil.run(this::loadHotbar);
+ }
+ }
+ }
+
+ public void loadHotbar() {
+ Player player = this.toPlayer();
+
+ if (player == null || !player.isOnline()) {
+ return;
+ }
+
+ PlayerUtil.reset(player, false);
+
+ if (this.isInLobby()) {
+ player.getInventory().setContents(PlayerHotbar.getLayout(PlayerHotbar.HotbarLayout.LOBBY, this));
+ } else if (this.isInQueue()) {
+ player.getInventory().setContents(PlayerHotbar.getLayout(PlayerHotbar.HotbarLayout.QUEUE, this));
+ } else if (this.isSpectating()) {
+ player.getInventory().setContents(PlayerHotbar.getLayout(PlayerHotbar.HotbarLayout.MATCH_SPECTATE, this));
+ } else if (this.isInEvent()) {
+ player.getInventory().setContents(PlayerHotbar.getLayout(PlayerHotbar.HotbarLayout.EVENT_SPECTATE, this));
+ }
+
+ player.updateInventory();
+ }
+
+ public NamedKit[] getKits(Ladder ladder) {
+ return this.kits.get(ladder.getName());
+ }
+
+ public NamedKit getKit(Ladder ladder, int index) {
+ return this.kits.get(ladder.getName())[index];
+ }
+
+ public void replaceKit(Ladder ladder, int index, NamedKit kit) {
+ NamedKit[] kits = this.kits.get(ladder.getName());
+ kits[index] = kit;
+
+ this.kits.put(ladder.getName(), kits);
+ }
+
+ public void deleteKit(Ladder ladder, NamedKit kit) {
+ if (kit == null) {
+ return;
+ }
+
+ NamedKit[] kits = this.kits.get(ladder.getName());
+
+ for (int i = 0; i < 4; i++) {
+ if (kits[i] != null && kits[i].equals(kit)) {
+ kits[i] = null;
+ break;
+ }
+ }
+
+ this.kits.put(ladder.getName(), kits);
+ }
+
+ public List getKitItems(Ladder ladder) {
+ List toReturn = new ArrayList<>();
+
+ toReturn.add(Kit.DEFAULT_KIT);
+
+ for (NamedKit kit : this.kits.get(ladder.getName())) {
+ if (kit != null) {
+ final ItemStack itemStack = new ItemStack(Material.ENCHANTED_BOOK);
+ final ItemMeta itemMeta = itemStack.getItemMeta();
+
+ itemMeta.setDisplayName(ChatColor.GOLD + "Kit: " + ChatColor.YELLOW + kit.getName());
+ itemMeta.setLore(Arrays.asList(
+ ChatColor.GRAY + "Right-click with this book in your",
+ ChatColor.GRAY + "hand to receive this kit."
+ ));
+ itemStack.setItemMeta(itemMeta);
+
+ toReturn.add(itemStack);
+ }
+ }
+
+ return toReturn;
+ }
+
+ public void load() {
+ try {
+ Document document = Praxi.getInstance().getPraxiMongo().getPlayer(this.getUuid());
+
+ if (document == null) {
+ this.loaded = true;
+ this.save();
+ return;
+ }
+
+ if (this.getName() == null) {
+ this.setName(document.getString("name"));
+ }
+
+ final Document statisticsDocument = (Document) document.get("statistics");
+ final Document laddersDocument = (Document) statisticsDocument.get("ladders");
+ final Document kitsDocument = (Document) document.get("kits");
+
+ for (String key : laddersDocument.keySet()) {
+ final Document ladderDocument = (Document) laddersDocument.get(key);
+ final Ladder ladder = Ladder.getByName(key);
+
+ if (ladder == null) {
+ continue;
+ }
+
+ LadderStatistics ladderStatistics = new LadderStatistics();
+
+ ladderStatistics.setElo(ladderDocument.getInteger("elo"));
+ ladderStatistics.setWon(ladderDocument.getInteger("won"));
+ ladderStatistics.setLost(ladderDocument.getInteger("lost"));
+
+ this.statistics.getLadders().put(ladder.getName(), ladderStatistics);
+ }
+
+ for (String key : kitsDocument.keySet()) {
+ Ladder ladder = Ladder.getByName(key);
+
+ if (ladder == null) {
+ continue;
+ }
+
+ JsonArray kitsArray = Praxi.PARSER.parse(kitsDocument.getString(key)).getAsJsonArray();
+ NamedKit[] kits = new NamedKit[4];
+
+ for (JsonElement kitElement : kitsArray) {
+ JsonObject kitObject = kitElement.getAsJsonObject();
+
+ NamedKit kit = new NamedKit(kitObject.get("name").getAsString());
+
+ kit.setArmor(InventoryUtil.deserializeInventory(kitObject.get("armor").getAsString()));
+ kit.setContents(InventoryUtil.deserializeInventory(kitObject.get("contents").getAsString()));
+
+ kits[kitObject.get("index").getAsInt()] = kit;
+ }
+
+ this.kits.put(ladder.getName(), kits);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+
+ this.loaded = true;
+ }
+
+ public void save() {
+ Document laddersDocument = new Document();
+
+ for (Map.Entry entry : this.statistics.getLadders().entrySet()) {
+ Document ladder = new Document();
+
+ ladder.put("elo", entry.getValue().getElo());
+ ladder.put("won", entry.getValue().getWon());
+ ladder.put("lost", entry.getValue().getLost());
+
+ laddersDocument.put(entry.getKey(), ladder);
+ }
+
+ Document statisticsDocument = new Document();
+
+ statisticsDocument.put("ladders", laddersDocument);
+
+ Document kitsDocument = new Document();
+
+ for (Map.Entry entry : this.kits.entrySet()) {
+ JsonArray kitsArray = new JsonArray();
+
+ for (int i = 0; i < 4; i++) {
+ NamedKit kit = entry.getValue()[i];
+
+ if (kit != null) {
+ JsonObject kitObject = new JsonObject();
+
+ kitObject.addProperty("index", i);
+ kitObject.addProperty("name", kit.getName());
+ kitObject.addProperty("armor", InventoryUtil.serializeInventory(kit.getArmor()));
+ kitObject.addProperty("contents", InventoryUtil.serializeInventory(kit.getContents()));
+
+ kitsArray.add(kitObject);
+ }
+ }
+
+ kitsDocument.put(entry.getKey(), kitsArray.toString());
+ }
+
+ Document document = new Document();
+
+ document.put("uuid", this.getUuid().toString());
+ document.put("name", this.getName());
+ document.put("statistics", statisticsDocument);
+ document.put("kits", kitsDocument);
+
+ Praxi.getInstance().getPraxiMongo().replacePlayer(this, document);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/RematchData.java b/plugin/src/main/java/me/joeleoli/praxi/player/RematchData.java
new file mode 100644
index 0000000..9232da6
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/RematchData.java
@@ -0,0 +1,127 @@
+package me.joeleoli.praxi.player;
+
+import java.util.UUID;
+import lombok.Getter;
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.chat.ChatComponentBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.impl.SoloMatch;
+import net.md_5.bungee.api.chat.ClickEvent;
+import net.md_5.bungee.api.chat.HoverEvent;
+import org.bukkit.entity.Player;
+
+@Getter
+public class RematchData {
+
+ private static final HoverEvent HOVER_EVENT = new HoverEvent(
+ HoverEvent.Action.SHOW_TEXT,
+ new ChatComponentBuilder(Style.YELLOW + "Click to accept this rematch invite.").create()
+ );
+ private static final ClickEvent CLICK_EVENT = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/rematch");
+
+ private UUID key;
+ private UUID sender;
+ private UUID target;
+ private Ladder ladder;
+ private Arena arena;
+ private boolean sent;
+ private boolean receive;
+ private long timestamp = System.currentTimeMillis();
+
+ public RematchData(UUID key, UUID sender, UUID target, Ladder ladder, Arena arena) {
+ this.key = key;
+ this.sender = sender;
+ this.target = target;
+ this.ladder = ladder;
+ this.arena = arena;
+ }
+
+ public void request() {
+
+ final Player sender = Praxi.getInstance().getServer().getPlayer(this.sender);
+ final Player target = Praxi.getInstance().getServer().getPlayer(this.target);
+
+ if (sender == null || target == null) {
+ return;
+ }
+
+ final PraxiPlayer senderPraxiPlayer = PraxiPlayer.getByUuid(sender.getUniqueId());
+ final PraxiPlayer targetPraxiPlayer = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (senderPraxiPlayer.getRematchData() == null || targetPraxiPlayer.getRematchData() == null ||
+ !senderPraxiPlayer.getRematchData().getKey().equals(targetPraxiPlayer.getRematchData().getKey())) {
+ return;
+ }
+
+ if (senderPraxiPlayer.isBusy()) {
+ sender.sendMessage(Style.RED + "You cannot duel right now.");
+ return;
+ }
+
+ sender.sendMessage(Style.translate(
+ "&eYou sent a rematch request to &d" + target.getName() + " &eon arena &d" + this.arena.getName() +
+ "&e."));
+ target.sendMessage(Style.translate(
+ "&d" + sender.getName() + " &ehas sent you a rematch request on arena &d" + this.arena.getName() +
+ "&e."));
+ target.sendMessage(new ChatComponentBuilder("").parse("&6Click here or type &b/rematch &6to accept the invite.")
+ .attachToEachPart(HOVER_EVENT).attachToEachPart(CLICK_EVENT)
+ .create());
+
+ this.sent = true;
+ targetPraxiPlayer.getRematchData().receive = true;
+
+ senderPraxiPlayer.refreshHotbar();
+ targetPraxiPlayer.refreshHotbar();
+ }
+
+ public void accept() {
+ final Player sender = Praxi.getInstance().getServer().getPlayer(this.sender);
+ final Player target = Praxi.getInstance().getServer().getPlayer(this.target);
+
+ if (sender == null || target == null || !sender.isOnline() || !target.isOnline()) {
+ return;
+ }
+
+ final PraxiPlayer senderPraxiPlayer = PraxiPlayer.getByUuid(sender.getUniqueId());
+ final PraxiPlayer targetPraxiPlayer = PraxiPlayer.getByUuid(target.getUniqueId());
+
+ if (senderPraxiPlayer.getRematchData() == null || targetPraxiPlayer.getRematchData() == null ||
+ !senderPraxiPlayer.getRematchData().getKey().equals(targetPraxiPlayer.getRematchData().getKey())) {
+ return;
+ }
+
+ if (senderPraxiPlayer.isBusy()) {
+ sender.sendMessage(Style.RED + "You cannot duel right now.");
+ return;
+ }
+
+ if (targetPraxiPlayer.isBusy()) {
+ sender.sendMessage(NucleusAPI.getColoredName(target) + Style.RED + " is currently busy.");
+ return;
+ }
+
+ Arena arena = this.arena;
+
+ if (arena.isActive()) {
+ arena = Arena.getRandom(this.ladder);
+ }
+
+ if (arena == null) {
+ sender.sendMessage(Style.RED + "Tried to start a match but there are no available arenas.");
+ return;
+ }
+
+ arena.setActive(true);
+
+ Match match = new SoloMatch(new MatchPlayer(sender), new MatchPlayer(target), this.ladder, arena, false, true);
+
+ match.handleStart();
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/player/gui/PlayerSettingsMenu.java b/plugin/src/main/java/me/joeleoli/praxi/player/gui/PlayerSettingsMenu.java
new file mode 100644
index 0000000..ed79786
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/player/gui/PlayerSettingsMenu.java
@@ -0,0 +1,151 @@
+package me.joeleoli.praxi.player.gui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.player.DefinedSetting;
+import me.joeleoli.nucleus.player.NucleusPlayer;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TextSplitter;
+import me.joeleoli.praxi.player.PracticeSetting;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+public class PlayerSettingsMenu extends Menu {
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.PINK + Style.BOLD + "Your Settings";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ final Map buttons = new HashMap<>();
+
+ for (SettingInfo settingInfo : SettingInfo.values()) {
+ buttons.put(buttons.size(), new ToggleButton(settingInfo));
+ }
+
+ return buttons;
+ }
+
+ @AllArgsConstructor
+ private enum SettingInfo {
+ SCOREBOARD(
+ PracticeSetting.SHOW_SCOREBOARD,
+ Style.PINK + Style.BOLD + "Scoreboard",
+ "If enabled, information will be displayed on your side scoreboard.",
+ Material.ITEM_FRAME,
+ "Show your scoreboard",
+ "Hide your scoreboard"
+ ),
+ SPECTATORS(
+ PracticeSetting.ALLOW_SPECTATORS,
+ Style.AQUA + Style.BOLD + "Spectators",
+ "If enabled, players can spectate your match with /spectate.",
+ Material.REDSTONE_TORCH_ON,
+ "Let players spectate your matches",
+ "Don't let players spectate your matches"
+ ),
+ DUEL_REQUESTS(
+ PracticeSetting.RECEIVE_DUEL_REQUESTS,
+ Style.RED + Style.BOLD + "Duel Requests",
+ "If enabled, players can request you duel requests.",
+ Material.BLAZE_ROD,
+ "Let players request you duel requests",
+ "Don't let players request you duel requests"
+ ),
+ GLOBAL_MESSAGES(
+ DefinedSetting.GlobalPlayerSetting.RECEIVE_GLOBAL_MESSAGES,
+ Style.GREEN + Style.BOLD + "Global Messages",
+ "If enabled, you will receive global chat messages.",
+ Material.BOOK_AND_QUILL,
+ "Receive global chat messages",
+ "Don't receive global chat message"
+ ),
+ PRIVATE_MESSAGES(
+ DefinedSetting.GlobalPlayerSetting.RECEIVE_PRIVATE_MESSAGES,
+ Style.BLUE + Style.BOLD + "Private Messages",
+ "If enabled, you will receive private chat messages.",
+ Material.NAME_TAG,
+ "Receive private chat messages",
+ "Don't receive private chat message"
+ ),
+ MESSAGE_SOUNDS(
+ DefinedSetting.GlobalPlayerSetting.PLAY_MESSAGE_SOUNDS,
+ Style.YELLOW + Style.BOLD + "Message Sounds",
+ "If enabled, you will be notified via sound when you receive private messages.",
+ Material.RECORD_7,
+ "Play message sounds",
+ "Don't play message sounds"
+ ),
+ PING_FACTOR(
+ PracticeSetting.PING_FACTOR,
+ Style.DARK_PURPLE + Style.BOLD + "Ping Factor",
+ "If enabled, you will only be matched against players that have a similar ping to you.",
+ Material.EYE_OF_ENDER,
+ "Be matched against players with similar ping",
+ "Be matched against players with any ping"
+ );
+
+ private DefinedSetting setting;
+ private String title;
+ private String description;
+ private Material material;
+ private String enabledDescription;
+ private String disabledDescription;
+
+ public void toggle(Player player) {
+ final NucleusPlayer nucleusPlayer = NucleusPlayer.getByUuid(player.getUniqueId());
+ nucleusPlayer.getSettings().getSettings().put(this.setting, !this.get(player));
+ }
+
+ public boolean get(Player player) {
+ final NucleusPlayer nucleusPlayer = NucleusPlayer.getByUuid(player.getUniqueId());
+ return nucleusPlayer.getSettings().getBoolean(this.setting);
+ }
+ }
+
+ @AllArgsConstructor
+ private static class ToggleButton extends Button {
+
+ private SettingInfo settingInfo;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ final List lore = new ArrayList<>();
+
+ lore.add("");
+ lore.addAll(TextSplitter.split(this.settingInfo.description, Style.YELLOW));
+ lore.add("");
+ lore.add(" " + (this.settingInfo.get(player) ? Style.GREEN + Style.UNICODE_ARROWS_RIGHT : " ") + " " +
+ Style.GOLD + this.settingInfo.enabledDescription);
+ lore.add(" " + (!this.settingInfo.get(player) ? Style.GREEN + Style.UNICODE_ARROWS_RIGHT : " ") + " " +
+ Style.GOLD + this.settingInfo.disabledDescription);
+
+ return new ItemBuilder(this.settingInfo.material)
+ .name(this.settingInfo.title)
+ .lore(lore)
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hbSlot) {
+ this.settingInfo.toggle(player);
+ }
+
+ @Override
+ public boolean shouldUpdate(Player player, int slot, ClickType clickType) {
+ return true;
+ }
+
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/queue/Queue.java b/plugin/src/main/java/me/joeleoli/praxi/queue/Queue.java
new file mode 100644
index 0000000..a6002b6
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/queue/Queue.java
@@ -0,0 +1,94 @@
+package me.joeleoli.praxi.queue;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.Predicate;
+import lombok.Getter;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.ladder.Ladder;
+import me.joeleoli.praxi.player.PlayerState;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+@Getter
+public class Queue {
+
+ @Getter
+ private static List queues = new ArrayList<>();
+
+ private UUID uuid = UUID.randomUUID();
+ private Ladder ladder;
+ private boolean ranked;
+ private LinkedList players;
+
+ public Queue(Ladder ladder, boolean ranked) {
+ this.ladder = ladder;
+ this.ranked = ranked;
+ this.players = new LinkedList<>();
+
+ queues.add(this);
+ }
+
+ public static Queue getByUuid(UUID uuid) {
+ for (Queue queue : queues) {
+ if (queue.getUuid().equals(uuid)) {
+ return queue;
+ }
+ }
+
+ return null;
+ }
+
+ public static Queue getByPredicate(Predicate predicate) {
+ for (Queue queue : queues) {
+ if (predicate.test(queue)) {
+ return queue;
+ }
+ }
+
+ return null;
+ }
+
+ public void addPlayer(Player player, int elo) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+ final QueuePlayer queuePlayer = new QueuePlayer(this.uuid, player.getUniqueId());
+
+ if (this.ranked) {
+ queuePlayer.setElo(elo);
+ }
+
+ praxiPlayer.setState(PlayerState.IN_QUEUE);
+ praxiPlayer.setQueuePlayer(queuePlayer);
+ praxiPlayer.loadHotbar();
+
+ player.sendMessage(
+ Style.YELLOW + "You joined the " + Style.PINK + (this.ranked ? "Ranked" : "Unranked") + " " +
+ this.ladder.getName() + Style.YELLOW + " queue.");
+
+ this.players.add(queuePlayer);
+ }
+
+ public QueuePlayer removePlayer(QueuePlayer queuePlayer) {
+ this.players.remove(queuePlayer);
+
+ final Player player = Bukkit.getPlayer(queuePlayer.getPlayerUuid());
+
+ if (player != null && player.isOnline()) {
+ player.sendMessage(
+ Style.YELLOW + "You left the " + Style.PINK + (this.ranked ? "Ranked" : "Unranked") + " " +
+ this.ladder.getName() + Style.YELLOW + " queue.");
+ }
+
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(queuePlayer.getPlayerUuid());
+
+ praxiPlayer.setQueuePlayer(null);
+ praxiPlayer.setState(PlayerState.IN_LOBBY);
+ praxiPlayer.loadHotbar();
+
+ return queuePlayer;
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/queue/QueuePlayer.java b/plugin/src/main/java/me/joeleoli/praxi/queue/QueuePlayer.java
new file mode 100644
index 0000000..a9f0baf
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/queue/QueuePlayer.java
@@ -0,0 +1,59 @@
+package me.joeleoli.praxi.queue;
+
+import java.util.UUID;
+import lombok.Data;
+
+@Data
+public class QueuePlayer {
+
+ private UUID queueUuid;
+ private UUID playerUuid;
+ private int elo;
+ private int range = 25;
+ private long start = System.currentTimeMillis();
+ private int ticked;
+
+ public QueuePlayer(UUID queueUuid, UUID playerUuid) {
+ this.queueUuid = queueUuid;
+ this.playerUuid = playerUuid;
+ }
+
+ public void tickRange() {
+ this.ticked++;
+
+ if (this.ticked >= 20) {
+ this.range += 25;
+ this.ticked = 0;
+ }
+ }
+
+ public Queue getQueue() {
+ return Queue.getByUuid(this.queueUuid);
+ }
+
+ public boolean isInRange(int elo) {
+ return elo >= (this.elo - this.range) && elo <= (this.elo + this.range);
+ }
+
+ public long getPassed() {
+ return System.currentTimeMillis() - this.start;
+ }
+
+ public int getMinRange() {
+ int min = this.elo - this.range;
+
+ return min < 0 ? 0 : min;
+ }
+
+ public int getMaxRange() {
+ int max = this.elo + this.range;
+
+ return max > 2500 ? 2500 : max;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof QueuePlayer && ((QueuePlayer) o).getPlayerUuid().equals(this.playerUuid);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/queue/QueueThread.java b/plugin/src/main/java/me/joeleoli/praxi/queue/QueueThread.java
new file mode 100644
index 0000000..e293ff1
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/queue/QueueThread.java
@@ -0,0 +1,135 @@
+package me.joeleoli.praxi.queue;
+
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.nucleus.util.TaskUtil;
+import me.joeleoli.praxi.arena.Arena;
+import me.joeleoli.praxi.match.Match;
+import me.joeleoli.praxi.match.MatchPlayer;
+import me.joeleoli.praxi.match.impl.SoloMatch;
+import me.joeleoli.praxi.player.PracticeSetting;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public class QueueThread extends Thread {
+
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ for (Queue queue : Queue.getQueues()) {
+ queue.getPlayers().forEach(QueuePlayer::tickRange);
+
+ if (queue.getPlayers().size() < 2) {
+ continue;
+ }
+
+ for (QueuePlayer firstQueuePlayer : queue.getPlayers()) {
+ final Player firstPlayer = Bukkit.getPlayer(firstQueuePlayer.getPlayerUuid());
+
+ if (firstPlayer == null) {
+ continue;
+ }
+
+ final PraxiPlayer firstPraxiPlayer = PraxiPlayer.getByUuid(firstQueuePlayer.getPlayerUuid());
+
+ for (QueuePlayer secondQueuePlayer : queue.getPlayers()) {
+ if (firstQueuePlayer.equals(secondQueuePlayer)) {
+ continue;
+ }
+
+ final Player secondPlayer = Bukkit.getPlayer(secondQueuePlayer.getPlayerUuid());
+ final PraxiPlayer secondPraxiPlayer =
+ PraxiPlayer.getByUuid(secondQueuePlayer.getPlayerUuid());
+
+ if (secondPlayer == null) {
+ continue;
+ }
+
+ if (NucleusAPI.getSetting(firstPlayer, PracticeSetting.PING_FACTOR) ||
+ NucleusAPI.getSetting(secondPlayer, PracticeSetting.PING_FACTOR)) {
+ if (firstPlayer.getPing() >= secondPlayer.getPing()) {
+ if (firstPlayer.getPing() - secondPlayer.getPing() >= 50) {
+ continue;
+ }
+ } else {
+ if (secondPlayer.getPing() - firstPlayer.getPing() >= 50) {
+ continue;
+ }
+ }
+ }
+
+ if (queue.isRanked()) {
+ if (!firstQueuePlayer.isInRange(secondQueuePlayer.getElo()) ||
+ !secondQueuePlayer.isInRange(firstQueuePlayer.getElo())) {
+ continue;
+ }
+ }
+
+ // Find arena
+ final Arena arena = Arena.getRandom(queue.getLadder());
+
+ if (arena == null) {
+ continue;
+ }
+
+ // Update arena
+ arena.setActive(true);
+
+ // Remove players from queue
+ queue.getPlayers().remove(firstQueuePlayer);
+ queue.getPlayers().remove(secondQueuePlayer);
+
+ final MatchPlayer firstMatchPlayer = new MatchPlayer(firstPlayer);
+ final MatchPlayer secondMatchPlayer = new MatchPlayer(secondPlayer);
+
+ if (queue.isRanked()) {
+ firstMatchPlayer.setElo(firstPraxiPlayer.getStatistics().getElo(queue.getLadder()));
+ secondMatchPlayer.setElo(secondPraxiPlayer.getStatistics().getElo(queue.getLadder()));
+ }
+
+ // Create match
+ final Match match = new SoloMatch(queue.getUuid(), firstMatchPlayer, secondMatchPlayer,
+ queue.getLadder(), arena, queue.isRanked(), false
+ );
+
+ final String[] opponentMessages =
+ this.formatOpponentMessages(firstPlayer.getName(), secondPlayer.getName(),
+ firstMatchPlayer.getElo(), secondMatchPlayer.getElo(), queue.isRanked()
+ );
+
+ firstPlayer.sendMessage(opponentMessages[0]);
+ secondPlayer.sendMessage(opponentMessages[1]);
+
+ TaskUtil.run(match::handleStart);
+ }
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ continue;
+ }
+
+ try {
+ Thread.sleep(200L);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private String[] formatOpponentMessages(String player1, String player2, int player1Elo, int player2Elo,
+ boolean ranked) {
+ final String player1Format = player1 + (ranked ? Style.GRAY + " (" + player1Elo + ")" : "");
+ final String player2Format = player2 + (ranked ? Style.GRAY + " (" + player2Elo + ")" : "");
+
+ return new String[]{
+ Style.YELLOW + Style.BOLD + "Found opponent: " + Style.GREEN + player1Format + Style.PINK + " vs. " +
+ Style.RED + player2Format,
+ Style.YELLOW + Style.BOLD + "Found opponent: " + Style.GREEN + player2Format + Style.PINK + " vs. " +
+ Style.RED + player1Format
+ };
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/queue/gui/QueueJoinMenu.java b/plugin/src/main/java/me/joeleoli/praxi/queue/gui/QueueJoinMenu.java
new file mode 100644
index 0000000..84bce15
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/queue/gui/QueueJoinMenu.java
@@ -0,0 +1,92 @@
+package me.joeleoli.praxi.queue.gui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import me.joeleoli.nucleus.NucleusAPI;
+import me.joeleoli.nucleus.menu.Button;
+import me.joeleoli.nucleus.menu.Menu;
+import me.joeleoli.nucleus.util.ItemBuilder;
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import me.joeleoli.praxi.queue.Queue;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.ClickType;
+import org.bukkit.inventory.ItemStack;
+
+@AllArgsConstructor
+public class QueueJoinMenu extends Menu {
+
+ private boolean ranked;
+
+ @Override
+ public String getTitle(Player player) {
+ return Style.GOLD + "Join " + (this.ranked ? "Ranked" : "Unranked") + " Queue";
+ }
+
+ @Override
+ public Map getButtons(Player player) {
+ Map buttons = new HashMap<>();
+
+ int i = 0;
+
+ for (Queue queue : Queue.getQueues()) {
+ if (queue.isRanked() == this.ranked) {
+ buttons.put(i++, new SelectLadderButton(queue));
+ }
+ }
+
+ return buttons;
+ }
+
+ @AllArgsConstructor
+ private class SelectLadderButton extends Button {
+
+ private Queue queue;
+
+ @Override
+ public ItemStack getButtonItem(Player player) {
+ final List lore = new ArrayList<>();
+
+ lore.add(Style.YELLOW + "Fighting: " + Style.RESET + Praxi.getInstance().getFightingCount(this.queue));
+ lore.add(Style.YELLOW + "Queueing: " + Style.RESET + this.queue.getPlayers().size());
+ lore.add("");
+ lore.add(Style.YELLOW + "Click here to select " + Style.PINK + Style.BOLD +
+ this.queue.getLadder().getName() + Style.YELLOW + ".");
+
+ return new ItemBuilder(this.queue.getLadder().getDisplayIcon())
+ .name(Style.PINK + Style.BOLD + this.queue.getLadder().getName()).lore(lore)
+ .build();
+ }
+
+ @Override
+ public void clicked(Player player, int slot, ClickType clickType, int hotbarButton) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if (praxiPlayer == null) {
+ return;
+ }
+
+ if (NucleusAPI.isFrozen(player)) {
+ player.sendMessage(Style.RED + "You cannot queue while frozen.");
+ return;
+ }
+
+ if (praxiPlayer.isBusy()) {
+ player.sendMessage(Style.RED + "You cannot queue right now.");
+ return;
+ }
+
+ player.closeInventory();
+
+ this.queue.addPlayer(
+ player,
+ !this.queue.isRanked() ? 0 : praxiPlayer.getStatistics().getElo(this.queue.getLadder())
+ );
+ }
+
+ }
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/task/ExpBarCooldownTask.java b/plugin/src/main/java/me/joeleoli/praxi/task/ExpBarCooldownTask.java
new file mode 100644
index 0000000..18109bc
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/task/ExpBarCooldownTask.java
@@ -0,0 +1,37 @@
+package me.joeleoli.praxi.task;
+
+import me.joeleoli.nucleus.util.Style;
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+
+public class ExpBarCooldownTask implements Runnable {
+
+ @Override
+ public void run() {
+ for (Player player : Praxi.getInstance().getServer().getOnlinePlayers()) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ if ((praxiPlayer.isInMatch() || praxiPlayer.isInEvent()) && !praxiPlayer.getEnderpearlCooldown().hasExpired()) {
+ int seconds = Math.round(praxiPlayer.getEnderpearlCooldown().getRemaining()) / 1_000;
+
+ player.setLevel(seconds);
+ player.setExp(praxiPlayer.getEnderpearlCooldown().getRemaining() / 16_000.0F);
+ } else {
+ if (!praxiPlayer.getEnderpearlCooldown().isNotified()) {
+ player.sendMessage(Style.PINK + "Your pearl cooldown has expired.");
+ praxiPlayer.getEnderpearlCooldown().setNotified(true);
+ }
+
+ if (player.getLevel() > 0) {
+ player.setLevel(0);
+ }
+
+ if (player.getExp() > 0.0F) {
+ player.setExp(0.0F);
+ }
+ }
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/task/InventoryCleanupTask.java b/plugin/src/main/java/me/joeleoli/praxi/task/InventoryCleanupTask.java
new file mode 100644
index 0000000..08a6383
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/task/InventoryCleanupTask.java
@@ -0,0 +1,13 @@
+package me.joeleoli.praxi.task;
+
+import me.joeleoli.praxi.match.MatchSnapshot;
+
+public class InventoryCleanupTask implements Runnable {
+
+ @Override
+ public void run() {
+ MatchSnapshot.getCache().entrySet()
+ .removeIf(entry -> System.currentTimeMillis() - entry.getValue().getCreated() >= 45_000);
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/task/InviteCleanupTask.java b/plugin/src/main/java/me/joeleoli/praxi/task/InviteCleanupTask.java
new file mode 100644
index 0000000..1feb5bd
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/task/InviteCleanupTask.java
@@ -0,0 +1,13 @@
+package me.joeleoli.praxi.task;
+
+import me.joeleoli.praxi.party.Party;
+
+public class InviteCleanupTask implements Runnable {
+
+ @Override
+ public void run() {
+ Party.getParties().forEach(party -> party.getInvited().entrySet().removeIf(
+ entry -> System.currentTimeMillis() >= entry.getValue() + 30_000));
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/task/RematchExpireRunnable.java b/plugin/src/main/java/me/joeleoli/praxi/task/RematchExpireRunnable.java
new file mode 100644
index 0000000..93bb327
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/task/RematchExpireRunnable.java
@@ -0,0 +1,18 @@
+package me.joeleoli.praxi.task;
+
+import me.joeleoli.praxi.Praxi;
+import me.joeleoli.praxi.player.PraxiPlayer;
+import org.bukkit.entity.Player;
+
+public class RematchExpireRunnable implements Runnable {
+
+ @Override
+ public void run() {
+ for (Player player : Praxi.getInstance().getServer().getOnlinePlayers()) {
+ final PraxiPlayer praxiPlayer = PraxiPlayer.getByUuid(player.getUniqueId());
+
+ praxiPlayer.refreshHotbar();
+ }
+ }
+
+}
diff --git a/plugin/src/main/java/me/joeleoli/praxi/task/SaveDataTask.java b/plugin/src/main/java/me/joeleoli/praxi/task/SaveDataTask.java
new file mode 100644
index 0000000..cbf48c9
--- /dev/null
+++ b/plugin/src/main/java/me/joeleoli/praxi/task/SaveDataTask.java
@@ -0,0 +1,14 @@
+package me.joeleoli.praxi.task;
+
+import me.joeleoli.praxi.player.PraxiPlayer;
+
+public class SaveDataTask implements Runnable {
+
+ @Override
+ public void run() {
+ for (PraxiPlayer praxiPlayer : PraxiPlayer.getPlayers().values()) {
+ praxiPlayer.save();
+ }
+ }
+
+}
diff --git a/plugin/src/main/resources/arenas.yml b/plugin/src/main/resources/arenas.yml
new file mode 100644
index 0000000..e69de29
diff --git a/plugin/src/main/resources/config.yml b/plugin/src/main/resources/config.yml
new file mode 100644
index 0000000..8a08ca2
--- /dev/null
+++ b/plugin/src/main/resources/config.yml
@@ -0,0 +1,9 @@
+mongo:
+ host: "127.0.0.1"
+ port: 27017
+ database: "praxi"
+ authentication:
+ enabled: false
+ username: "admin"
+ password: "dev"
+ database: "admin"
\ No newline at end of file
diff --git a/plugin/src/main/resources/ladders.yml b/plugin/src/main/resources/ladders.yml
new file mode 100644
index 0000000..cb2fb79
--- /dev/null
+++ b/plugin/src/main/resources/ladders.yml
@@ -0,0 +1,81 @@
+ladders:
+ Classic:
+ enabled: true
+ build: false
+ sumo: false
+ parkour: false
+ regeneration: true
+ display-name: "&eClassic"
+ display-icon:
+ material: IRON_SWORD
+ durability: 0
+ kit-editor:
+ allow-potion-fill: true
+ items:
+ 1:
+ material: COOKED_BEEF
+ amount: 64
+ 2:
+ material: GOLDEN_CARROT
+ amount: 64
+ 3:
+ material: POTION
+ durability: 16388
+ 4:
+ material: POTION
+ durability: 16426
+ 5:
+ material: POTION
+ durability: 16421
+ 6:
+ material: POTION
+ durability: 8226
+ NoDebuff:
+ enabled: true
+ build: false
+ sumo: false
+ parkour: false
+ regeneration: true
+ display-name: "&6NoDebuff"
+ display-icon:
+ material: DIAMOND_SWORD
+ durability: 0
+ kit-editor:
+ allow-potion-fill: true
+ items:
+ 1:
+ material: COOKED_BEEF
+ durability: 0
+ amount: 64
+ 2:
+ material: GOLDEN_CARROT
+ durability: 0
+ amount: 64
+ 3:
+ material: POTION
+ durability: 16421
+ amount: 1
+ 4:
+ material: POTION
+ durability: 8226
+ amount: 1
+ BuildUHC:
+ enabled: true
+ build: true
+ sumo: false
+ parkour: false
+ regeneration: false
+ display-name: "&eBuildUHC"
+ display-icon:
+ material: LAVA_BUCKET
+ durability: 0
+ Axe:
+ enabled: true
+ build: false
+ sumo: false
+ parkour: false
+ regeneration: true
+ display-name: "&bAxe"
+ display-icon:
+ material: IRON_AXE
+ durability: 0
\ No newline at end of file
diff --git a/plugin/src/main/resources/plugin.yml b/plugin/src/main/resources/plugin.yml
new file mode 100644
index 0000000..c2ba0a8
--- /dev/null
+++ b/plugin/src/main/resources/plugin.yml
@@ -0,0 +1,6 @@
+main: me.joeleoli.praxi.Praxi
+name: Praxi
+version: ${project.version}
+author: joeleoli
+description: ${project.description}
+depend: [Nucleus]
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..81b851d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ me.joeleoli.praxi-optimized
+ parent
+ pom
+ 1.0-SNAPSHOT
+
+
+ plugin
+
+
+
+
+ md5-repo
+ http://repo.md-5.net/content/groups/public/
+
+
+ dmulloy2-repo
+ http://repo.dmulloy2.net/nexus/repository/public/
+
+
+
+
+
+ me.joeleoli.ragespigot
+ ragespigot
+ 1.8.8-R0.1-SNAPSHOT
+ provided
+
+
+ org.projectlombok
+ lombok
+ 1.16.16
+ provided
+
+
+
+
+ clean install
+ praxi-${project.name}
+
+
+
+ src/main/resources
+ true
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.6.1
+
+
+
+ 1.8
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.0.0
+
+ false
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+
+
+
+
\ No newline at end of file