Add /hg stuck command for teleporting players to the nearest safe location with configurable cooldown and effects

This commit is contained in:
akastijn 2026-06-15 22:37:16 +02:00
parent 4c1fd4d228
commit 016ab17ef2
6 changed files with 189 additions and 27 deletions

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

10
TODO.md
View File

@ -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)

View File

@ -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("<commands>", subCommands.stream()
.filter(subCommand -> commandSender.hasPermission(subCommand.getPermission()))
commandSender.sendRichMessage(Messages.HELP.HELP_MESSAGE_WRAPPER.replaceAll("<commands>",
subCommands.stream()
.filter(subCommand -> commandSender.hasPermission(
subCommand.getPermission()))
.map(SubCommand::getHelpMessage)
.collect(Collectors.joining("\n"))));
.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;
}

View File

@ -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<UUID, Long> 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<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.STUCK;
}
}

View File

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

View File

@ -30,6 +30,7 @@ public class Messages extends AbstractConfig {
public static String REGISTER = "<green>Register a player for the game: <gold>/hg register <player></gold></green>";
public static String START_ROUND = "<green>Start the game: <gold>/hg start</gold></green>";
public static String RELOAD = "<green>Reload config and messages: <gold>/hg reload</gold></green>";
public static String STUCK = "<green>Teleport to safety if stuck: <gold>/hg stuck</gold></green>";
@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 = "<red>The round can not be started because the current state is <state>.</red>";
public static class STUCK {
private static final String prefix = "stuck.";
public static String TELEPORTED = "<green>You have been teleported to safety!</green>";
public static String TOO_LOW = "<red>You are too low to use this command (minimum height: <height>). If you are truly stuck here dm a staff member for help</red>";
public static String NO_SAFE_LOCATION = "<red>Unable to find a safe grass block nearby</red>";
public static String ON_COOLDOWN = "<red>You must wait <time> before using this command again</red>";
@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);
}
}
}