diff --git a/module/duels/build.gradle.kts b/module/duels/build.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/module/duels/src/main/java/org/battleplugins/arena/module/duels/Duels.java b/module/duels/src/main/java/org/battleplugins/arena/module/duels/Duels.java new file mode 100644 index 00000000..a98dfa29 --- /dev/null +++ b/module/duels/src/main/java/org/battleplugins/arena/module/duels/Duels.java @@ -0,0 +1,129 @@ +package org.battleplugins.arena.module.duels; + +import org.battleplugins.arena.Arena; +import org.battleplugins.arena.competition.Competition; +import org.battleplugins.arena.competition.JoinResult; +import org.battleplugins.arena.competition.LiveCompetition; +import org.battleplugins.arena.competition.PlayerRole; +import org.battleplugins.arena.competition.map.LiveCompetitionMap; +import org.battleplugins.arena.competition.map.MapType; +import org.battleplugins.arena.competition.phase.CompetitionPhaseType; +import org.battleplugins.arena.event.arena.ArenaCreateExecutorEvent; +import org.battleplugins.arena.event.player.ArenaPreJoinEvent; +import org.battleplugins.arena.messages.Messages; +import org.battleplugins.arena.module.ArenaModule; +import org.battleplugins.arena.module.ArenaModuleInitializer; +import org.battleplugins.arena.team.ArenaTeam; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.player.PlayerQuitEvent; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * A module that adds duels to BattleArena. + */ +@ArenaModule(id = Duels.ID, name = "Duels", description = "Adds duels to BattleArena.", authors = "BattlePlugins") +public class Duels implements ArenaModuleInitializer { + public static final String ID = "duels"; + public static final JoinResult PENDING_REQUEST = new JoinResult(false, DuelsMessages.PENDING_DUEL_REQUEST); + + private final Map duelRequests = new HashMap<>(); + + @EventHandler + public void onCreateExecutor(ArenaCreateExecutorEvent event) { + if (!event.getArena().isModuleEnabled(ID)) { + return; + } + + event.registerSubExecutor(new DuelsExecutor(this, event.getArena())); + } + + @EventHandler + public void onPlayerQuit(PlayerQuitEvent event) { + UUID requested = this.duelRequests.remove(event.getPlayer().getUniqueId()); + Player requestedPlayer = Bukkit.getPlayer(requested); + if (requestedPlayer != null) { + DuelsMessages.DUEL_REQUESTED_CANCELLED_QUIT.send(requestedPlayer, event.getPlayer().getName()); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + public void onPreJoin(ArenaPreJoinEvent event) { + if (this.duelRequests.containsKey(event.getPlayer().getUniqueId())) { + event.setResult(PENDING_REQUEST); + } + } + + public Map getDuelRequests() { + return Map.copyOf(this.duelRequests); + } + + public void addDuelRequest(UUID sender, UUID receiver) { + this.duelRequests.put(sender, receiver); + } + + public void removeDuelRequest(UUID sender) { + this.duelRequests.remove(sender); + } + + public void acceptDuel(Arena arena, Player player, Player target) { + LiveCompetition competition = findOrJoinCompetition(arena); + if (competition == null) { + Messages.NO_OPEN_ARENAS.send(player); + Messages.NO_OPEN_ARENAS.send(target); + return; + } + + // Non-team game - just join regularly and let game calculate team. Winner will be + // determined by the individual player who wins + if (arena.getTeams().isNonTeamGame()) { + competition.join(player, PlayerRole.PLAYING); + competition.join(target, PlayerRole.PLAYING); + } else { + ArenaTeam team1 = competition.getTeamManager().getTeams().iterator().next(); + ArenaTeam team2 = competition.getTeamManager().getTeams().iterator().next(); + + competition.join(player, PlayerRole.PLAYING, team1); + competition.join(target, PlayerRole.PLAYING, team2); + } + + // Force the game into the in-game state + competition.getPhaseManager().setPhase(CompetitionPhaseType.INGAME); + } + + private LiveCompetition findOrJoinCompetition(Arena arena) { + List> openCompetitions = arena.getPlugin().getCompetitions(arena) + .stream() + .filter(competition -> competition instanceof LiveCompetition liveCompetition + && liveCompetition.getPhaseManager().getCurrentPhase().canJoin() + && liveCompetition.getPlayers().isEmpty() + ) + .toList(); + + // Ensure we have found an open competition + if (openCompetitions.isEmpty()) { + List dynamicMaps = arena.getPlugin().getMaps(arena) + .stream() + .filter(map -> map.getType() == MapType.DYNAMIC) + .toList(); + + if (dynamicMaps.isEmpty()) { + return null; + } + + LiveCompetitionMap map = dynamicMaps.iterator().next(); + + LiveCompetition competition = map.createDynamicCompetition(arena); + arena.getPlugin().addCompetition(arena, competition); + return competition; + } else { + return (LiveCompetition) openCompetitions.iterator().next(); + } + } +} diff --git a/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsExecutor.java b/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsExecutor.java new file mode 100644 index 00000000..b5843e67 --- /dev/null +++ b/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsExecutor.java @@ -0,0 +1,133 @@ +package org.battleplugins.arena.module.duels; + +import org.battleplugins.arena.Arena; +import org.battleplugins.arena.ArenaPlayer; +import org.battleplugins.arena.BattleArena; +import org.battleplugins.arena.command.ArenaCommand; +import org.battleplugins.arena.command.SubCommandExecutor; +import org.battleplugins.arena.messages.Messages; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.Locale; +import java.util.UUID; + +public class DuelsExecutor implements SubCommandExecutor { + private final Duels module; + private final Arena arena; + private final String parentCommand; + + public DuelsExecutor(Duels module, Arena arena) { + this.module = module; + this.arena = arena; + this.parentCommand = arena.getName().toLowerCase(Locale.ROOT); + } + + @ArenaCommand(commands = "duel", description = "Duel another player.", permissionNode = "duel") + public void duel(Player player, Player target) { + if (player.equals(target)) { + DuelsMessages.CANNOT_DUEL_SELF.send(player); + return; + } + + ArenaPlayer arenaPlayer = ArenaPlayer.getArenaPlayer(player); + if (arenaPlayer != null) { + Messages.ALREADY_IN_ARENA.send(player); + return; + } + + ArenaPlayer targetPlayer = ArenaPlayer.getArenaPlayer(target); + if (targetPlayer != null) { + Messages.ALREADY_IN_ARENA.send(player); + return; + } + + if (this.module.getDuelRequests().containsKey(player.getUniqueId())) { + DuelsMessages.DUEL_REQUEST_ALREADY_SENT.send(player, this.parentCommand); + return; + } + + DuelsMessages.DUEL_REQUEST_SENT.send(player, target.getName()); + DuelsMessages.DUEL_REQUEST_RECEIVED.send( + target, + player.getName(), + this.parentCommand, + player.getName(), + this.parentCommand, + player.getName() + ); + + this.module.addDuelRequest(player.getUniqueId(), target.getUniqueId()); + } + + @ArenaCommand(commands = "duel", subCommands = "accept", description = "Accept a duel request.", permissionNode = "duel.accept") + public void acceptDuel(Player player, Player target) { + ArenaPlayer arenaPlayer = ArenaPlayer.getArenaPlayer(player); + if (arenaPlayer != null) { + Messages.ALREADY_IN_ARENA.send(player); + return; + } + + UUID requestedId = this.module.getDuelRequests().get(target.getUniqueId()); + if (requestedId == null) { + DuelsMessages.NO_DUEL_REQUESTS.send(player); + return; + } + + if (!requestedId.equals(player.getUniqueId())) { + DuelsMessages.USER_DID_NOT_REQUEST.send(player, target.getName()); + return; + } + + DuelsMessages.DUEL_REQUEST_ACCEPTED.send(player, target.getName()); + DuelsMessages.ACCEPTED_DUEL_REQUEST.send(target, player.getName()); + + this.module.removeDuelRequest(target.getUniqueId()); + + Bukkit.getScheduler().runTaskLater(BattleArena.getInstance(), () -> { + this.module.acceptDuel(this.arena, player, target); + }, 100); + } + + @ArenaCommand(commands = "duel", subCommands = "deny", description = "Deny a duel request.", permissionNode = "duel.deny") + public void denyDuel(Player player, Player target) { + UUID requestedId = this.module.getDuelRequests().get(target.getUniqueId()); + if (requestedId == null) { + DuelsMessages.NO_DUEL_REQUESTS.send(player); + return; + } + + if (!requestedId.equals(player.getUniqueId())) { + DuelsMessages.USER_DID_NOT_REQUEST.send(player, target.getName()); + return; + } + + DuelsMessages.DUEL_REQUEST_DENIED.send(player, target.getName()); + DuelsMessages.DENIED_DUEL_REQUEST.send(target, player.getName()); + + this.module.removeDuelRequest(target.getUniqueId()); + } + + @ArenaCommand(commands = "duel", subCommands = "cancel", description = "Cancel a duel request.", permissionNode = "duel.cancel") + public void cancelDuel(Player player) { + if (!this.module.getDuelRequests().containsKey(player.getUniqueId())) { + DuelsMessages.NO_DUEL_REQUESTS.send(player); + return; + } + + UUID targetId = this.module.getDuelRequests().get(player.getUniqueId()); + Player target = Bukkit.getServer().getPlayer(targetId); + if (target == null) { + // Shouldn't get here but just incase + this.module.removeDuelRequest(player.getUniqueId()); + + DuelsMessages.NO_DUEL_REQUESTS.send(player); + return; + } + + DuelsMessages.DUEL_REQUEST_CANCELLED.send(player, target.getName()); + DuelsMessages.CANCELLED_DUEL_REQUEST.send(target, player.getName()); + + this.module.removeDuelRequest(player.getUniqueId()); + } +} diff --git a/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsMessages.java b/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsMessages.java new file mode 100644 index 00000000..493433ff --- /dev/null +++ b/module/duels/src/main/java/org/battleplugins/arena/module/duels/DuelsMessages.java @@ -0,0 +1,21 @@ +package org.battleplugins.arena.module.duels; + +import org.battleplugins.arena.messages.Message; +import org.battleplugins.arena.messages.Messages; + +public final class DuelsMessages { + public static final Message CANNOT_DUEL_SELF = Messages.error("duel-cannot-duel-self", "You cannot duel yourself!"); + public static final Message DUEL_REQUEST_SENT = Messages.info("duel-request-sent", "Duel request sent to {}!"); + public static final Message DUEL_REQUEST_RECEIVED = Messages.info("duel-request-received", "You have received a duel request from {}! Type /{} duel accept {} to accept or /{} duel deny {} to deny."); + public static final Message DUEL_REQUEST_ALREADY_SENT = Messages.error("duel-request-already-sent", "You have already have an outgoing duel request! Type /{} duel cancel to cancel."); + public static final Message NO_DUEL_REQUESTS = Messages.error("duel-no-requests", "You have no duel requests!"); + public static final Message USER_DID_NOT_REQUEST = Messages.error("duel-user-did-not-request", "{} did not request a duel with you! You can only accept requests from the user who sent it."); + public static final Message DUEL_REQUEST_DENIED = Messages.info("duel-request-denied", "You have denied the duel request from {}!"); + public static final Message DENIED_DUEL_REQUEST = Messages.error("duel-denied-request", "{} has denied your duel request!"); + public static final Message DUEL_REQUEST_CANCELLED = Messages.info("duel-request-cancelled", "You have cancelled your duel request to {}!"); + public static final Message CANCELLED_DUEL_REQUEST = Messages.error("duel-cancelled-request", "{} has cancelled their duel request!"); + public static final Message DUEL_REQUESTED_CANCELLED_QUIT = Messages.info("duel-request-cancelled-quit", "Your duel request from {} has been cancelled as they have left the server."); + public static final Message DUEL_REQUEST_ACCEPTED = Messages.info("duel-request-accepted", "You have accepted the duel request from {}! The duel will commence in 5 seconds!"); + public static final Message ACCEPTED_DUEL_REQUEST = Messages.info("duel-accepted-request", "{} has accepted your duel request! The duel will commence in 5 seconds!"); + public static final Message PENDING_DUEL_REQUEST = Messages.error("duel-pending-request", "You have a pending outgoing duel request! Type /arena duel cancel to cancel."); +} diff --git a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommand.java b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommand.java index fb07320c..6901ad02 100644 --- a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommand.java +++ b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommand.java @@ -12,8 +12,8 @@ int maxArgs() default -1; boolean overrideDisabled() default false; - boolean requiresOp() default false; + String permissionNode() default ""; String description() default ""; diff --git a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java index 13a56528..caed5af0 100644 --- a/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java +++ b/plugin/src/main/java/org/battleplugins/arena/command/ArenaCommandExecutor.java @@ -1,7 +1,6 @@ package org.battleplugins.arena.command; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import org.battleplugins.arena.Arena; import org.battleplugins.arena.ArenaPlayer; @@ -33,8 +32,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; public class ArenaCommandExecutor extends BaseCommandExecutor { protected final Arena arena; diff --git a/plugin/src/main/java/org/battleplugins/arena/competition/map/LiveCompetitionMap.java b/plugin/src/main/java/org/battleplugins/arena/competition/map/LiveCompetitionMap.java index b415d102..ac4a8765 100644 --- a/plugin/src/main/java/org/battleplugins/arena/competition/map/LiveCompetitionMap.java +++ b/plugin/src/main/java/org/battleplugins/arena/competition/map/LiveCompetitionMap.java @@ -85,7 +85,7 @@ public void postProcess() { * @param arena the arena to create the competition for * @return the created competition */ - public Competition createCompetition(Arena arena) { + public LiveCompetition createCompetition(Arena arena) { return new LiveCompetition<>(arena, arena.getType(), this); } @@ -224,7 +224,7 @@ public final void setSpawns(Spawns spawns) { * @return the created dynamic competition */ @Nullable - public final Competition createDynamicCompetition(Arena arena) { + public final LiveCompetition createDynamicCompetition(Arena arena) { if (this.type != MapType.DYNAMIC) { throw new IllegalStateException("Cannot create dynamic competition for non-dynamic map!"); } diff --git a/plugin/src/main/java/org/battleplugins/arena/editor/stage/SpawnInputStage.java b/plugin/src/main/java/org/battleplugins/arena/editor/stage/SpawnInputStage.java index 82c2ad78..bc3f5f58 100644 --- a/plugin/src/main/java/org/battleplugins/arena/editor/stage/SpawnInputStage.java +++ b/plugin/src/main/java/org/battleplugins/arena/editor/stage/SpawnInputStage.java @@ -37,7 +37,7 @@ public void onChatInput(String input) { @Override public boolean isValidChatInput(String input) { - return !input.startsWith("/") && SpawnInputStage.this.input.equalsIgnoreCase(input); + return super.isValidChatInput(input) && !input.startsWith("/") && SpawnInputStage.this.input.equalsIgnoreCase(input); } }.bind(context); } diff --git a/plugin/src/main/java/org/battleplugins/arena/editor/stage/TeamSpawnInputStage.java b/plugin/src/main/java/org/battleplugins/arena/editor/stage/TeamSpawnInputStage.java index 14afb604..e78d5b05 100644 --- a/plugin/src/main/java/org/battleplugins/arena/editor/stage/TeamSpawnInputStage.java +++ b/plugin/src/main/java/org/battleplugins/arena/editor/stage/TeamSpawnInputStage.java @@ -105,14 +105,14 @@ public void onChatInput(String input) { @Override public boolean isValidChatInput(String input) { - return !input.startsWith("/") && teamNames.contains(input); + return super.isValidChatInput(input) && (!input.startsWith("/") && teamNames.contains(input)); } }.bind(context); } @Override public boolean isValidChatInput(String input) { - return input.equalsIgnoreCase("clear") || input.equalsIgnoreCase("done") || (!input.startsWith("/") && TeamSpawnInputStage.this.input.equalsIgnoreCase(input)); + return super.isValidChatInput(input) && (input.equalsIgnoreCase("clear") || input.equalsIgnoreCase("done") || (!input.startsWith("/") && TeamSpawnInputStage.this.input.equalsIgnoreCase(input))); } }.bind(context); } diff --git a/plugin/src/main/java/org/battleplugins/arena/editor/stage/TextInputStage.java b/plugin/src/main/java/org/battleplugins/arena/editor/stage/TextInputStage.java index 1c35e2e6..5c54d353 100644 --- a/plugin/src/main/java/org/battleplugins/arena/editor/stage/TextInputStage.java +++ b/plugin/src/main/java/org/battleplugins/arena/editor/stage/TextInputStage.java @@ -42,7 +42,7 @@ public void onChatInput(String input) { @Override public boolean isValidChatInput(String input) { - return (!input.startsWith("/") && (validContentFunction == null || validContentFunction.apply(context, input))); + return super.isValidChatInput(input) && (!input.startsWith("/") && (validContentFunction == null || validContentFunction.apply(context, input))); } }.bind(context); } diff --git a/plugin/src/main/java/org/battleplugins/arena/util/InteractionInputs.java b/plugin/src/main/java/org/battleplugins/arena/util/InteractionInputs.java index 054b31ec..bd00c710 100644 --- a/plugin/src/main/java/org/battleplugins/arena/util/InteractionInputs.java +++ b/plugin/src/main/java/org/battleplugins/arena/util/InteractionInputs.java @@ -44,7 +44,7 @@ public ChatInput(Player player, Message invalidInput) { Listener createListener(Player player) { return new Listener() { - @EventHandler(ignoreCancelled = true) + @EventHandler public void onChat(AsyncChatEvent event) { if (!player.equals(event.getPlayer())) { return; @@ -53,6 +53,11 @@ public void onChat(AsyncChatEvent event) { event.setCancelled(true); String message = PlainTextComponentSerializer.plainText().serialize(event.originalMessage()); if (!isValidChatInput(message)) { + // Don't send feedback if the message is "cancel" + if (message.equalsIgnoreCase("cancel")) { + return; + } + if (invalidInput != null) { invalidInput.send(player); } @@ -85,7 +90,7 @@ public void onChat(AsyncChatEvent event) { * @return true if the input is valid, false otherwise */ public boolean isValidChatInput(String input) { - return true; + return !input.contains("cancel"); } } diff --git a/plugin/src/main/resources/arenas/arena.yml b/plugin/src/main/resources/arenas/arena.yml index 4bf4ec4a..db7bb9a8 100644 --- a/plugin/src/main/resources/arenas/arena.yml +++ b/plugin/src/main/resources/arenas/arena.yml @@ -8,6 +8,7 @@ team-options: modules: - arena-restoration - classes + - duels - scoreboards lives: enabled: false diff --git a/settings.gradle.kts b/settings.gradle.kts index d2eb1f64..b6dcb464 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -21,6 +21,7 @@ include("plugin") include("module:arena-restoration") include("module:boundary-enforcer") include("module:classes") +include("module:duels") include("module:one-in-the-chamber") include("module:scoreboards") include("module:team-colors")