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