From 016ab17ef2f673fe2680921ce9d5f45efccc2da2 Mon Sep 17 00:00:00 2001 From: akastijn Date: Mon, 15 Jun 2026 22:37:16 +0200 Subject: [PATCH] Add `/hg stuck` command for teleporting players to the nearest safe location with configurable cooldown and effects --- .idea/codeStyles/codeStyleConfig.xml | 5 + TODO.md | 10 +- .../hunger_games/commands/BaseCommand.java | 40 +++--- .../commands/subcommands/Stuck.java | 136 ++++++++++++++++++ .../com/alttd/hunger_games/config/Config.java | 8 ++ .../alttd/hunger_games/config/Messages.java | 17 ++- 6 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 src/main/java/com/alttd/hunger_games/commands/subcommands/Stuck.java diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/TODO.md b/TODO.md index 3bb9a0f..f6073bc 100644 --- a/TODO.md +++ b/TODO.md @@ -139,11 +139,11 @@ Track each registered player in one of these states: ## 🧭 Commands ### `/hg stuck` -- [ ] Teleport player to the nearest safe grass block -- [ ] Require minimum height threshold to prevent abuse (configurable) -- [ ] Launch fireworks at the destination on use -- [ ] Configure firework count/type -- [ ] Optional cooldown to prevent spam +- [x] Teleport player to the nearest safe grass block +- [x] Require minimum height threshold to prevent abuse (configurable) +- [x] Launch fireworks at the destination on use +- [x] Configure firework count/type +- [x] Optional cooldown to prevent spam ### `/hg stats [player]` - [ ] Show overall stats for the specified player (or self if no argument) diff --git a/src/main/java/com/alttd/hunger_games/commands/BaseCommand.java b/src/main/java/com/alttd/hunger_games/commands/BaseCommand.java index aff77dc..af61cc6 100644 --- a/src/main/java/com/alttd/hunger_games/commands/BaseCommand.java +++ b/src/main/java/com/alttd/hunger_games/commands/BaseCommand.java @@ -1,10 +1,7 @@ package com.alttd.hunger_games.commands; import com.alttd.hunger_games.Main; -import com.alttd.hunger_games.commands.subcommands.Register; -import com.alttd.hunger_games.commands.subcommands.Reload; -import com.alttd.hunger_games.commands.subcommands.RoundState; -import com.alttd.hunger_games.commands.subcommands.StartRound; +import com.alttd.hunger_games.commands.subcommands.*; import com.alttd.hunger_games.config.Messages; import com.alttd.hunger_games.services.PlayerService; import com.alttd.hunger_games.services.Round; @@ -40,17 +37,22 @@ public class BaseCommand implements CommandExecutor, TabExecutor { new Reload(main), new RoundState(roundService), new Register(playerService), - new StartRound(round, roundService) - )); + new StartRound(round, roundService), + new Stuck() + )); } @Override public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, @NotNull String cmd, @NotNull String[] args) { if (args.length == 0) { - commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("", subCommands.stream() - .filter(subCommand -> commandSender.hasPermission(subCommand.getPermission())) - .map(SubCommand::getHelpMessage) - .collect(Collectors.joining("\n")))); + commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("", + subCommands.stream() + .filter(subCommand -> commandSender.hasPermission( + subCommand.getPermission())) + .map(SubCommand::getHelpMessage) + .collect(Collectors.joining( + "\n")) + )); return true; } @@ -60,7 +62,9 @@ public class BaseCommand implements CommandExecutor, TabExecutor { } if (!commandSender.hasPermission(subCommand.getPermission())) { - commandSender.sendRichMessage(Messages.GENERIC.NO_PERMISSION, Placeholder.parsed("permission", subCommand.getPermission())); + commandSender.sendRichMessage(Messages.GENERIC.NO_PERMISSION, + Placeholder.parsed("permission", subCommand.getPermission()) + ); return true; } @@ -77,17 +81,17 @@ public class BaseCommand implements CommandExecutor, TabExecutor { if (args.length <= 1) { res.addAll(subCommands.stream() - .filter(subCommand -> commandSender.hasPermission(subCommand.getPermission())) - .map(SubCommand::getName) - .filter(name -> args.length == 0 || name.startsWith(args[0])) - .toList() - ); + .filter(subCommand -> commandSender.hasPermission(subCommand.getPermission())) + .map(SubCommand::getName) + .filter(name -> args.length == 0 || name.startsWith(args[0])) + .toList() + ); } else { SubCommand subCommand = getSubCommand(args[0]); if (subCommand != null && commandSender.hasPermission(subCommand.getPermission())) { res.addAll(subCommand.getTabComplete(commandSender, args).stream() - .filter(str -> str.toLowerCase().startsWith(args[args.length - 1].toLowerCase())) - .toList()); + .filter(str -> str.toLowerCase().startsWith(args[args.length - 1].toLowerCase())) + .toList()); } } return res; diff --git a/src/main/java/com/alttd/hunger_games/commands/subcommands/Stuck.java b/src/main/java/com/alttd/hunger_games/commands/subcommands/Stuck.java new file mode 100644 index 0000000..0ecc9ff --- /dev/null +++ b/src/main/java/com/alttd/hunger_games/commands/subcommands/Stuck.java @@ -0,0 +1,136 @@ +package com.alttd.hunger_games.commands.subcommands; + +import com.alttd.hunger_games.commands.SubCommand; +import com.alttd.hunger_games.config.Config; +import com.alttd.hunger_games.config.Messages; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import org.bukkit.Color; +import org.bukkit.FireworkEffect; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Firework; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.FireworkMeta; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +public class Stuck extends SubCommand { + + private final Map cooldowns = new HashMap<>(); + + @Override + public boolean onCommand(CommandSender commandSender, String[] args) { + if (!(commandSender instanceof Player player)) { + commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY); + return true; + } + + if (player.getLocation().getY() < Config.DESTINATION.STUCK_MIN_HEIGHT) { + player.sendRichMessage(Messages.STUCK.TOO_LOW, + Placeholder.parsed("height", String.valueOf(Config.DESTINATION.STUCK_MIN_HEIGHT))); + return true; + } + + if (isOnCooldown(player)) { + long remaining = getRemainingCooldown(player); + player.sendRichMessage(Messages.STUCK.ON_COOLDOWN, + Placeholder.parsed("time", remaining + "s")); + return true; + } + + Location safeLocation = findSafeLocation(player.getLocation()); + if (safeLocation == null) { + player.sendRichMessage(Messages.STUCK.NO_SAFE_LOCATION); + return true; + } + + player.teleport(safeLocation.add(0.5, 1, 0.5)); + spawnFireworks(player.getLocation()); + player.sendRichMessage(Messages.STUCK.TELEPORTED); + cooldowns.put(player.getUniqueId(), System.currentTimeMillis()); + + return true; + } + + private boolean isOnCooldown(Player player) { + if (!cooldowns.containsKey(player.getUniqueId())) { + return false; + } + long lastUse = cooldowns.get(player.getUniqueId()); + return (System.currentTimeMillis() - lastUse) < (Config.DESTINATION.STUCK_COOLDOWN_SECONDS * 1000L); + } + + private long getRemainingCooldown(Player player) { + if (!cooldowns.containsKey(player.getUniqueId())) { + return 0; + } + long lastUse = cooldowns.get(player.getUniqueId()); + long elapsed = (System.currentTimeMillis() - lastUse) / 1000; + return Math.max(0, Config.DESTINATION.STUCK_COOLDOWN_SECONDS - elapsed); + } + + private Location findSafeLocation(Location start) { + int radius = 10; + for (int r = 0; r <= radius; r++) { + for (int x = -r; x <= r; x++) { + for (int z = -r; z <= r; z++) { + if (Math.abs(x) != r && Math.abs(z) != r) continue; + + Block block = start.clone().add(x, 0, z).getBlock(); + // Search up and down a bit from current Y + for (int yOffset = -5; yOffset <= 5; yOffset++) { + Block target = block.getRelative(0, yOffset, 0); + if (isSafe(target)) { + return target.getLocation(); + } + } + } + } + } + return null; + } + + private boolean isSafe(Block block) { + return block.getType() == Material.GRASS_BLOCK && + block.getRelative(BlockFace.UP).getType().isAir() && + block.getRelative(BlockFace.UP).getRelative(BlockFace.UP).getType().isAir(); + } + + private void spawnFireworks(Location location) { + for (int i = 0; i < Config.DESTINATION.STUCK_FIREWORK_COUNT; i++) { + Firework firework = (Firework) location.getWorld().spawnEntity(location, EntityType.FIREWORK_ROCKET); + FireworkMeta meta = firework.getFireworkMeta(); + meta.addEffect(FireworkEffect.builder() + .withColor(Color.RED, Color.ORANGE, Color.YELLOW) + .withFade(Color.ORANGE) + .with(FireworkEffect.Type.BALL_LARGE) + .trail(true) + .flicker(true) + .build()); + meta.setPower(1); + firework.setFireworkMeta(meta); + } + } + + @Override + public String getName() { + return "stuck"; + } + + @Override + public List getTabComplete(CommandSender commandSender, String[] args) { + return List.of(); + } + + @Override + public String getHelpMessage() { + return Messages.HELP.STUCK; + } +} diff --git a/src/main/java/com/alttd/hunger_games/config/Config.java b/src/main/java/com/alttd/hunger_games/config/Config.java index 2a9f086..5b89e46 100644 --- a/src/main/java/com/alttd/hunger_games/config/Config.java +++ b/src/main/java/com/alttd/hunger_games/config/Config.java @@ -110,6 +110,10 @@ public class Config extends AbstractConfig { public static int FINALE_RADIUS = 10; public static Location SPECTATOR_AREA = null; + public static int STUCK_MIN_HEIGHT = 64; + public static int STUCK_FIREWORK_COUNT = 3; + public static int STUCK_COOLDOWN_SECONDS = 300; + @SuppressWarnings("unused") private static void load() { Config.DESTINATION.START_RADIUS = config.getInt(prefix, "start-radius", START_RADIUS); @@ -122,6 +126,10 @@ public class Config extends AbstractConfig { DESTINATION.FINALE_RADIUS = config.getInt(prefix, "finale-radius", FINALE_RADIUS); config.getLocation(prefix, "finale-center") .ifPresent(location -> DESTINATION.FINALE_CENTER = location); + + STUCK_MIN_HEIGHT = config.getInt(prefix, "stuck-min-height", STUCK_MIN_HEIGHT); + STUCK_FIREWORK_COUNT = config.getInt(prefix, "stuck-firework-count", STUCK_FIREWORK_COUNT); + STUCK_COOLDOWN_SECONDS = config.getInt(prefix, "stuck-cooldown-seconds", STUCK_COOLDOWN_SECONDS); } } } diff --git a/src/main/java/com/alttd/hunger_games/config/Messages.java b/src/main/java/com/alttd/hunger_games/config/Messages.java index 2d1f46e..1b9ad64 100644 --- a/src/main/java/com/alttd/hunger_games/config/Messages.java +++ b/src/main/java/com/alttd/hunger_games/config/Messages.java @@ -30,6 +30,7 @@ public class Messages extends AbstractConfig { public static String REGISTER = "Register a player for the game: /hg register "; public static String START_ROUND = "Start the game: /hg start"; public static String RELOAD = "Reload config and messages: /hg reload"; + public static String STUCK = "Teleport to safety if stuck: /hg stuck"; @SuppressWarnings("unused") private static void load() { @@ -39,6 +40,7 @@ public class Messages extends AbstractConfig { REGISTER = config.getString(prefix, "register", REGISTER); START_ROUND = config.getString(prefix, "start", START_ROUND); RELOAD = config.getString(prefix, "reload", RELOAD); + STUCK = config.getString(prefix, "stuck", STUCK); } } @@ -119,13 +121,20 @@ public class Messages extends AbstractConfig { } } - public static class START_ROUND { - private static final String prefix = "start-round."; - public static String CAN_NOT_START_ROUND = "The round can not be started because the current state is ."; + public static class STUCK { + private static final String prefix = "stuck."; + + public static String TELEPORTED = "You have been teleported to safety!"; + public static String TOO_LOW = "You are too low to use this command (minimum height: ). If you are truly stuck here dm a staff member for help"; + public static String NO_SAFE_LOCATION = "Unable to find a safe grass block nearby"; + public static String ON_COOLDOWN = "You must wait "; @SuppressWarnings("unused") private static void load() { - CAN_NOT_START_ROUND = config.getString(prefix, "can-not-start", CAN_NOT_START_ROUND); + TELEPORTED = config.getString(prefix, "teleported", TELEPORTED); + TOO_LOW = config.getString(prefix, "too-low", TOO_LOW); + NO_SAFE_LOCATION = config.getString(prefix, "no-safe-location", NO_SAFE_LOCATION); + ON_COOLDOWN = config.getString(prefix, "on-cooldown", ON_COOLDOWN); } } }