Compare commits

..

3 Commits

6 changed files with 287 additions and 27 deletions

View File

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

11
TODO.md
View File

@ -139,11 +139,12 @@ 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
- [x] Warmup period (configurable) that cancels on movement or damage
### `/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(main)
));
}
@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,223 @@
package com.alttd.hunger_games.commands.subcommands;
import com.alttd.hunger_games.Main;
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.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.inventory.meta.FireworkMeta;
import org.bukkit.scheduler.BukkitTask;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
public class Stuck extends SubCommand implements Listener {
private final Main main;
private final Map<UUID, Instant> cooldowns = new HashMap<>();
private final Map<UUID, Warmup> warmups = new HashMap<>();
public Stuck(Main main) {
this.main = main;
main.getServer().getPluginManager().registerEvents(this, main);
}
private record Warmup(Location location, BukkitTask task) { }
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
if (!(commandSender instanceof Player player)) {
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
return true;
}
if (warmups.containsKey(player.getUniqueId())) {
player.sendRichMessage(Messages.STUCK.ALREADY_WARMING_UP);
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)) {
Duration remaining = getRemainingCooldown(player);
player.sendRichMessage(Messages.STUCK.ON_COOLDOWN,
Placeholder.parsed("time", remaining.toSeconds() + "s")
);
return true;
}
Optional<Location> safeLocation = findSafeLocation(player.getLocation());
if (safeLocation.isEmpty()) {
player.sendRichMessage(Messages.STUCK.NO_SAFE_LOCATION);
return true;
}
startWarmup(player, safeLocation.get());
return true;
}
private void startWarmup(Player player, Location safeLocation) {
Duration warmupTime = Config.DESTINATION.STUCK_WARMUP;
if (warmupTime.isZero() || warmupTime.isNegative()) {
teleport(player, safeLocation);
return;
}
player.sendRichMessage(Messages.STUCK.WARMUP_STARTED,
Placeholder.parsed("time", warmupTime.toSeconds() + "s")
);
BukkitTask task = main.getServer().getScheduler().runTaskLater(main, () -> {
warmups.remove(player.getUniqueId());
teleport(player, safeLocation);
}, warmupTime.toSeconds() * 20L);
warmups.put(player.getUniqueId(), new Warmup(player.getLocation(), task));
}
private void teleport(Player player, Location safeLocation) {
player.teleport(safeLocation.add(0.5, 1, 0.5));
spawnFireworks(player.getLocation());
player.sendRichMessage(Messages.STUCK.TELEPORTED);
cooldowns.put(player.getUniqueId(), Instant.now());
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
Warmup warmup = warmups.get(uuid);
if (warmup == null) {
return;
}
Location from = warmup.location();
Location to = event.getTo();
if (from.getWorld() != to.getWorld() || from.distanceSquared(to) > 0.25) {
warmup.task().cancel();
warmups.remove(uuid);
event.getPlayer().sendRichMessage(Messages.STUCK.WARMUP_CANCELLED_MOVEMENT);
}
}
@EventHandler
public void onPlayerDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player player)) {
return;
}
Warmup warmup = warmups.get(player.getUniqueId());
if (warmup == null) {
return;
}
warmup.task().cancel();
warmups.remove(player.getUniqueId());
player.sendRichMessage(Messages.STUCK.WARMUP_CANCELLED_DAMAGE);
}
private boolean isOnCooldown(Player player) {
Instant lastUse = cooldowns.get(player.getUniqueId());
if (lastUse == null) {
return false;
}
return Duration.between(lastUse, Instant.now()).compareTo(Config.DESTINATION.STUCK_COOLDOWN) < 0;
}
private Duration getRemainingCooldown(Player player) {
Instant lastUse = cooldowns.get(player.getUniqueId());
if (lastUse == null) {
return Duration.ZERO;
}
Duration elapsed = Duration.between(lastUse, Instant.now());
Duration remaining = Config.DESTINATION.STUCK_COOLDOWN.minus(elapsed);
return remaining.isNegative() ? Duration.ZERO : remaining;
}
private Optional<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;
}
Location baseLocation = start.clone().add(x, 0, z);
Optional<Location> safeY = findSafeY(baseLocation);
if (safeY.isPresent()) {
return safeY;
}
}
}
}
return Optional.empty();
}
private Optional<Location> findSafeY(Location base) {
// Search up and down a bit from current Y
for (int yOffset = -5; yOffset <= 5; yOffset++) {
Block target = base.getBlock().getRelative(0, yOffset, 0);
if (isSafe(target)) {
return Optional.of(target.getLocation());
}
}
return Optional.empty();
}
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,11 @@ 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 Duration STUCK_COOLDOWN = Duration.ofMinutes(2);
public static Duration STUCK_WARMUP = Duration.ofSeconds(5);
@SuppressWarnings("unused")
private static void load() {
Config.DESTINATION.START_RADIUS = config.getInt(prefix, "start-radius", START_RADIUS);
@ -122,6 +127,11 @@ 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 = Duration.ofSeconds(config.getInt(prefix, "stuck-cooldown-seconds", (int) STUCK_COOLDOWN.toSeconds()));
STUCK_WARMUP = Duration.ofSeconds(config.getInt(prefix, "stuck-warmup-seconds", (int) STUCK_WARMUP.toSeconds()));
}
}
}

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,28 @@ 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>";
public static String WARMUP_STARTED = "<green>Teleporting in <time>. Do not move or take damage!</green>";
public static String WARMUP_CANCELLED_MOVEMENT = "<red>Teleportation cancelled due to movement.</red>";
public static String WARMUP_CANCELLED_DAMAGE = "<red>Teleportation cancelled due to taking damage.</red>";
public static String ALREADY_WARMING_UP = "<red>You are already warming up!</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);
WARMUP_STARTED = config.getString(prefix, "warmup-started", WARMUP_STARTED);
WARMUP_CANCELLED_MOVEMENT = config.getString(prefix, "warmup-cancelled-movement", WARMUP_CANCELLED_MOVEMENT);
WARMUP_CANCELLED_DAMAGE = config.getString(prefix, "warmup-cancelled-damage", WARMUP_CANCELLED_DAMAGE);
ALREADY_WARMING_UP = config.getString(prefix, "already-warming-up", ALREADY_WARMING_UP);
}
}
}