Compare commits

...
20 Commits
Author SHA1 Message Date
stijn 7dce9d9f30 Integrate TabListService to update player colors during the game 2026-07-12 21:41:26 +02:00
stijn 95219c17bc Add Multiverse-Core as a soft dependency to ensure teleport locations are found 2026-07-11 22:22:09 +02:00
stijn d412812dcb Add /hg list command to display players and spectators in the game 2026-07-11 22:20:09 +02:00
stijn 21f12904ac Update stateChange to handle ROUND_STATE.ENDED instead of ROUND_STATE.FINALE. 2026-07-11 22:20:03 +02:00
stijn 416c44a8f3 show participant status in tab using colors
announce killer if present
show everyone the bossbar
drop items on death
give players participation price permission
2026-07-11 22:13:52 +02:00
stijn fc7b5c76a6 Handle ROUND_STATE.ENDED in stateChange to clear resources and reset loot. 2026-07-11 21:53:40 +02:00
stijn 9198af6c29 Register online players without auto-join-bypass permission during ROUND_STATE.PLAYER_REGISTRATION or ROUND_STATE.ENDED. 2026-07-11 19:38:59 +02:00
stijn d5266b659b Register online players without auto-join-bypass permission during ROUND_STATE.PLAYER_REGISTRATION or ROUND_STATE.ENDED. 2026-07-11 19:38:26 +02:00
stijn e9e7c1cc80 Allow round to start from ROUND_STATE.ENDED and update related state handling 2026-07-11 19:35:33 +02:00
stijn 40f52a4a45 Update PlayerService to adjust PLAYER_STATE during ROUND_STATE.ENDED phase 2026-07-11 19:14:37 +02:00
stijn 3906bd747b Handle asynchronous player teleportation in PlayerTeleporterService with error handling and logging. 2026-07-11 17:14:32 +02:00
stijn 0d757aa2ab Remove redundant teleportation logging in PlayerService during finale phase. 2026-07-11 17:14:25 +02:00
stijn 5f35866e7a Make finale border size configurable via Config and apply it dynamically in RoundService. 2026-07-11 17:14:07 +02:00
stijn 491e6a6050 Make finale border size configurable via Config and apply it dynamically in RoundService. 2026-07-11 16:52:15 +02:00
stijn 244f8f6df7 Add logging to PlayerService for teleports, adjust ROUND_STATE handling, and use @Slf4j annotation for consistency. Update countdown logic in Round and refactor configuration keys for finale timing. 2026-07-11 16:51:04 +02:00
stijn fe10d57978 Show title messages during broadcasts to enhance player visibility. 2026-07-11 16:28:03 +02:00
stijn 8d05658214 Introduce LootItem record and refactor loot handling logic to support material-specific max amounts 2026-07-06 00:04:53 +02:00
stijn 7324177250 Change player game mode from SURVIVAL to ADVENTURE during round reset in RoundService 2026-07-05 23:59:49 +02:00
stijn c7161f707c Refactor Round phase transitions and countdown logic; consolidate messages, adjust state handling, and add configurable durations for safe and kill phases. 2026-07-05 23:42:20 +02:00
stijn 5ce26782f2 Add CommandListener to enforce command whitelist and block restricted commands 2026-07-05 23:19:49 +02:00
25 changed files with 426 additions and 105 deletions
@@ -4,11 +4,7 @@ import com.alttd.hunger_games.commands.BaseCommand;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.config.LootItems;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.event_listeners.ChestListener;
import com.alttd.hunger_games.event_listeners.ItemSpawnListener;
import com.alttd.hunger_games.event_listeners.PlayerDamageListener;
import com.alttd.hunger_games.event_listeners.PlayerDisconnectListener;
import com.alttd.hunger_games.event_listeners.PlayerJoinListener;
import com.alttd.hunger_games.event_listeners.*;
import com.alttd.hunger_games.services.*;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.plugin.PluginManager;
@@ -24,6 +20,7 @@ public final class Main extends JavaPlugin {
private LootService lootService;
private StatService statService;
private BossBarService bossBarService;
private TabListService tabListService;
@Override
public void onEnable() {
@@ -41,6 +38,8 @@ public final class Main extends JavaPlugin {
statService = new StatService(this, round);
playerTeleporterService = PlayerTeleporterService.createSingletonInstance();
roundService = RoundService.createSingletonInstance(round, this, lootService, statService, playerTeleporterService);
tabListService = new TabListService(round, roundService);
roundService.setTabListService(tabListService);
playerService = PlayerService.createSingletonInstance(round, roundService, playerTeleporterService);
bossBarService = BossBarService.createSingletonInstance(round, roundService);
@@ -54,16 +53,17 @@ public final class Main extends JavaPlugin {
}
private void registerCommands() {
BaseCommand command = new BaseCommand(this, roundService, playerService, round, statService);
BaseCommand command = new BaseCommand(this, roundService, playerService, round, statService, tabListService);
}
private void registerEvents() {
PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new PlayerDisconnectListener(playerService), this);
pluginManager.registerEvents(new PlayerJoinListener(playerService), this);
pluginManager.registerEvents(new PlayerJoinListener(playerService, tabListService), this);
pluginManager.registerEvents(new PlayerDamageListener(roundService, playerService, statService), this);
pluginManager.registerEvents(new ChestListener(roundService, lootService), this);
pluginManager.registerEvents(new ItemSpawnListener(roundService), this);
pluginManager.registerEvents(new CommandListener(), this);
}
public void reloadConfigs() {
@@ -3,10 +3,7 @@ package com.alttd.hunger_games.commands;
import com.alttd.hunger_games.Main;
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;
import com.alttd.hunger_games.services.RoundService;
import com.alttd.hunger_games.services.StatService;
import com.alttd.hunger_games.services.*;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -23,7 +20,7 @@ import java.util.stream.Collectors;
public class BaseCommand implements CommandExecutor, TabExecutor {
private final List<SubCommand> subCommands;
public BaseCommand(Main main, RoundService roundService, PlayerService playerService, Round round, StatService statService) {
public BaseCommand(Main main, RoundService roundService, PlayerService playerService, Round round, StatService statService, TabListService tablistService) {
PluginCommand command = main.getCommand("hungergames");
if (command == null) {
subCommands = null;
@@ -37,12 +34,13 @@ public class BaseCommand implements CommandExecutor, TabExecutor {
subCommands = new ArrayList<>(List.of(
new Reload(main),
new RoundState(roundService),
new Register(playerService),
new StartRound(round, roundService),
new Register(playerService, tablistService),
new StartRound(round, roundService, playerService),
new NextPhase(round),
new EndRound(round),
new Stuck(main),
new Stats(statService)
new Stats(statService),
new ListPlayers(roundService)
));
}
@@ -0,0 +1,71 @@
package com.alttd.hunger_games.commands.subcommands;
import com.alttd.hunger_games.commands.SubCommand;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.services.RoundService;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.*;
public class ListPlayers extends SubCommand {
private final RoundService roundService;
public ListPlayers(RoundService roundService) {
this.roundService = roundService;
}
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
Set<UUID> registeredUuids = roundService.getPlayers(PLAYER_STATE.REGISTERED);
Set<UUID> spectatingUuids = roundService.getPlayers(PLAYER_STATE.SPECTATING);
List<Component> registeredNames = registeredUuids.stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.map(Player::name)
.toList();
List<Component> spectatingNames = spectatingUuids.stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.map(Player::name)
.toList();
Component inGamePlayers = Component.join(JoinConfiguration.commas(true), registeredNames);
Component spectators = Component.join(JoinConfiguration.commas(true), spectatingNames);
int inGameCount = registeredNames.size();
int totalCount = inGameCount + spectatingNames.size();
commandSender.sendRichMessage(Messages.COMMANDS.LIST_PLAYERS,
Placeholder.unparsed("in_game", String.valueOf(inGameCount)),
Placeholder.unparsed("total", String.valueOf(totalCount)),
Placeholder.component("in_game_players", inGamePlayers),
Placeholder.component("spectators", spectators)
);
return true;
}
@Override
public String getName() {
return "list";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return Collections.emptyList();
}
@Override
public String getHelpMessage() {
return Messages.HELP.LIST;
}
}
@@ -16,7 +16,7 @@ public class NextPhase extends SubCommand {
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
round.forceNextPhase();
commandSender.sendRichMessage("<green>Skipping to the next phase/stage.</green>");
commandSender.sendRichMessage(Messages.COMMANDS.NEXT_PHASE_SUCCESS);
return true;
}
@@ -5,6 +5,8 @@ import com.alttd.hunger_games.commands.shared_parsers.OnlinePlayerParser;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.TabListService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
@@ -15,16 +17,14 @@ import javax.annotation.Nullable;
import java.util.List;
import java.util.Optional;
@RequiredArgsConstructor
@Slf4j
public class Register extends SubCommand {
private final static OnlinePlayerParser ONLINE_PLAYER_PARSER = new OnlinePlayerParser();
private final static int PLAYER_ARG = 1;
private final PlayerService playerService;
public Register(PlayerService playerService) {
this.playerService = playerService;
}
private final TabListService tabListService;
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
@@ -44,6 +44,7 @@ public class Register extends SubCommand {
playerToRegister.sendRichMessage(Messages.REGISTER.FAILED_TO_REGISTER_YOU);
} else {
notifyRegisteredPlayer(playerToRegister, nullablePlayerState);
tabListService.updatePlayerColor(playerToRegister);
}
if (commandSender instanceof Player commandPlayer) {
@@ -3,10 +3,12 @@ package com.alttd.hunger_games.commands.subcommands;
import com.alttd.hunger_games.commands.SubCommand;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.Round;
import com.alttd.hunger_games.services.RoundService;
import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import java.util.List;
@@ -16,11 +18,15 @@ public class StartRound extends SubCommand {
private final Round round;
private final RoundService roundService;
private final PlayerService playerService;
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
ROUND_STATE roundState = roundService.getRoundState();
if (roundState == ROUND_STATE.PLAYER_REGISTRATION) {
if (roundState == ROUND_STATE.PLAYER_REGISTRATION || roundState == ROUND_STATE.ENDED) {
Bukkit.getOnlinePlayers().stream()
.filter(player -> !player.hasPermission("hungergames.auto-join-bypass"))
.forEach(playerService::registerPlayer);
round.startRound();
} else {
commandSender.sendRichMessage(Messages.START_ROUND.CAN_NOT_START_ROUND,
@@ -2,16 +2,12 @@ package com.alttd.hunger_games.config;
import com.alttd.hunger_games.data_objects.GameStage;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.*;
@Slf4j
public class Config extends AbstractConfig {
@@ -44,8 +40,13 @@ public class Config extends AbstractConfig {
public static class ROUND {
private static final String prefix = "round.";
public static String HG_WORLD = "world";
public static Duration COUNTDOWN = Duration.ofSeconds(10);
public static Duration SAFE_PHASE = Duration.ofSeconds(30);
public static Duration TIME_TO_FINALE = Duration.ofMinutes(5);
public static int INITIAL_BORDER_SIZE = 5000;
public static int FINALE_BORDER_SIZE = 48;
public static int DEFAULT_BORDER_TRANSITION_SECONDS = 45;
public static List<GameStage> STAGES = List.of(
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(1000).build(),
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(750).build(),
@@ -54,9 +55,16 @@ public class Config extends AbstractConfig {
@SuppressWarnings("unused")
private static void load() {
HG_WORLD = config.getString(prefix, "hg-world", HG_WORLD);
int countdownSeconds = config.getInt(prefix, "countdown-seconds", Math.toIntExact(COUNTDOWN.toSeconds()));
COUNTDOWN = Duration.ofSeconds(countdownSeconds);
int safePhaseSeconds = config.getInt(prefix, "safe-phase-seconds", Math.toIntExact(SAFE_PHASE.toSeconds()));
SAFE_PHASE = Duration.ofSeconds(safePhaseSeconds);
int killPhaseSeconds = config.getInt(prefix, "time-to-finale-seconds", Math.toIntExact(TIME_TO_FINALE.toSeconds()));
TIME_TO_FINALE = Duration.ofSeconds(killPhaseSeconds);
INITIAL_BORDER_SIZE = config.getInt(prefix, "initial-border-size", INITIAL_BORDER_SIZE);
FINALE_BORDER_SIZE = config.getInt(prefix, "finale-border-size", FINALE_BORDER_SIZE);
DEFAULT_BORDER_TRANSITION_SECONDS = config.getInt(prefix, "default-border-transition-seconds", DEFAULT_BORDER_TRANSITION_SECONDS);
ConfigurationSection configurationSection = config.getConfigurationSection(prefix + "stages");
if (configurationSection == null) {
@@ -134,4 +142,17 @@ public class Config extends AbstractConfig {
STUCK_WARMUP = Duration.ofSeconds(config.getInt(prefix, "stuck-warmup-seconds", (int) STUCK_WARMUP.toSeconds()));
}
}
public static class COMMAND_WHITELIST {
private static final String prefix = "command-whitelist";
public static final List<String> PREFIXES = new ArrayList<>(List.of("msg", "ac", "gac", "tell", "r", "server", "lobby", "hub", "bayou", "creative", "hg", "lp"));
@SuppressWarnings("unused")
private static void load() {
List<String> stringList = config.getStringList(prefix, "prefix-list", PREFIXES);
PREFIXES.clear();
PREFIXES.addAll(stringList);
}
}
}
@@ -1,12 +1,16 @@
package com.alttd.hunger_games.config;
import com.alttd.hunger_games.data_objects.LootItem;
import com.alttd.hunger_games.data_objects.RARITY;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@Slf4j
public class LootItems extends AbstractConfig {
static LootItems config;
@@ -27,18 +31,40 @@ public class LootItems extends AbstractConfig {
public static class ITEMS {
private static final String prefix = "items.";
private static final HashMap<RARITY, List<Material>> ITEMS = new HashMap<>();
private static final HashMap<RARITY, List<LootItem>> ITEMS = new HashMap<>();
public static List<Material> get(RARITY rarity) {
public static List<LootItem> get(RARITY rarity) {
return ITEMS.get(rarity);
}
@SuppressWarnings("unused")
private static void load() {
for (RARITY rarity : RARITY.values()) {
List<String> materialNameList = ITEMS.getOrDefault(rarity, List.of()).stream().map(Material::name).toList();
List<LootItem> currentItems = ITEMS.getOrDefault(rarity, List.of());
List<String> materialNameList = currentItems.stream()
.map(item -> item.material().name() + (item.maxAmount() > 1 ? ":" + item.maxAmount() : ""))
.toList();
List<String> materialList = config.getList(prefix, rarity.getConfigName(), materialNameList);
ITEMS.put(rarity, materialList.stream().map(Material::getMaterial).toList());
List<LootItem> lootItems = new ArrayList<>();
for (String entry : materialList) {
String[] split = entry.split(":");
Material material = Material.getMaterial(split[0].toUpperCase());
if (material == null) {
log.warn("Invalid material found in loot-items.yml: {}", split[0]);
continue;
}
int maxAmount = 1;
if (split.length > 1) {
try {
maxAmount = Integer.parseInt(split[1]);
} catch (NumberFormatException ignored) {
log.error("Invalid max amount found in loot-items.yml: {}", split[1]);
}
}
lootItems.add(new LootItem(material, maxAmount));
}
ITEMS.put(rarity, lootItems);
}
}
}
@@ -33,6 +33,7 @@ public class Messages extends AbstractConfig {
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>";
public static String STATS = "<green>Show player stats: <gold>/hg stats [player]</gold></green>";
public static String LIST = "<green>List all players in the game and spectators: <gold>/hg list</gold></green>";
public static String END_ROUND = "<green>End the game: <gold>/hg end</gold></green>";
@SuppressWarnings("unused")
@@ -46,6 +47,7 @@ public class Messages extends AbstractConfig {
RELOAD = config.getString(prefix, "reload", RELOAD);
STUCK = config.getString(prefix, "stuck", STUCK);
STATS = config.getString(prefix, "stats", STATS);
LIST = config.getString(prefix, "list", LIST);
END_ROUND = config.getString(prefix, "end-round", END_ROUND);
}
}
@@ -57,6 +59,7 @@ public class Messages extends AbstractConfig {
public static String PLAYER_ONLY = "<red>This command can only be executed as a player</red>";
public static String PLAYER_NOT_FOUND = "<red>Unable to find online player <player></red>";
public static String RELOAD_SUCCESS = "<green>Config and messages have been reloaded.</green>";
public static String COMMAND_BLOCKED = "<red>You cannot use that command here!</red>";
@SuppressWarnings("unused")
private static void load() {
@@ -64,6 +67,7 @@ public class Messages extends AbstractConfig {
PLAYER_ONLY = config.getString(prefix, "player-only", PLAYER_ONLY);
PLAYER_NOT_FOUND = config.getString(prefix, "player-only", PLAYER_NOT_FOUND);
RELOAD_SUCCESS = config.getString(prefix, "reload-success", RELOAD_SUCCESS);
COMMAND_BLOCKED = config.getString(prefix, "command-blocked", COMMAND_BLOCKED);
}
}
@@ -87,8 +91,10 @@ public class Messages extends AbstractConfig {
public static String CHEST_EMPTY = "<red>This chest is empty!</red>";
public static String WINNER = "<gold><player> has won the Hunger Games!</gold>";
public static String PLAYER_DEATH = "<red><player> has died! <gold><remaining></gold> players remaining.</red>";
public static String PLAYER_KILLED = "<red><victim> was killed by <killer>! <gold><remaining></gold> players remaining.</red>";
public static String BOSSBAR_COUNTDOWN = "<gold>Loot phase in <time></gold>";
public static String BOSSBAR_PVP_COUNTDOWN = "<green>PVP starts in <time></green>";
public static String BOSSBAR_SAFE_PHASE = "<green>Border shrinking in <time></green>";
public static String BOSSBAR_KILL_PHASE = "<red>Border shrinking... <time></red>";
public static String BOSSBAR_FINALE = "<gold>Remaining Players: <remaining></gold>";
@@ -103,8 +109,10 @@ public class Messages extends AbstractConfig {
CHEST_EMPTY = config.getString(prefix, "chest-empty", CHEST_EMPTY);
WINNER = config.getString(prefix, "winner", WINNER);
PLAYER_DEATH = config.getString(prefix, "player-death", PLAYER_DEATH);
PLAYER_KILLED = config.getString(prefix, "player-killed", PLAYER_KILLED);
BOSSBAR_COUNTDOWN = config.getString(prefix, "bossbar-countdown", BOSSBAR_COUNTDOWN);
BOSSBAR_PVP_COUNTDOWN = config.getString(prefix, "bossbar-pvp-countdown", BOSSBAR_PVP_COUNTDOWN);
BOSSBAR_SAFE_PHASE = config.getString(prefix, "bossbar-border-shrinking-in", BOSSBAR_SAFE_PHASE);
BOSSBAR_KILL_PHASE = config.getString(prefix, "bossbar-border-shrinking", BOSSBAR_KILL_PHASE);
BOSSBAR_FINALE = config.getString(prefix, "bossbar-finale", BOSSBAR_FINALE);
@@ -199,4 +207,20 @@ public class Messages extends AbstractConfig {
STATS_FORMAT = config.getString(prefix, "format", STATS_FORMAT);
}
}
public static class COMMANDS {
private static final String prefix = "commands.";
public static String NEXT_PHASE_SUCCESS = "<green>Skipping to the next phase/stage.</green>";
public static String LIST_PLAYERS = """
<gold>HungerGames Players (<in_game>/<total>):</gold>
<green>In Game: <white><in_game_players></white></green>
<yellow>Spectators: <white><spectators></white></yellow>""";
@SuppressWarnings("unused")
private static void load() {
NEXT_PHASE_SUCCESS = config.getString(prefix + "next-phase", "success", NEXT_PHASE_SUCCESS);
LIST_PLAYERS = config.getString(prefix, "list-players", LIST_PLAYERS);
}
}
}
@@ -0,0 +1,6 @@
package com.alttd.hunger_games.data_objects;
import org.bukkit.Material;
public record LootItem(Material material, int maxAmount) {
}
@@ -0,0 +1,34 @@
package com.alttd.hunger_games.event_listeners;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.config.Messages;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import java.util.Optional;
public class CommandListener implements Listener {
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
Player player = event.getPlayer();
if (player.hasPermission("hg.bypass")) {
return;
}
// Remove leading slash
String message = event.getMessage().substring(1).toLowerCase();
String command = message.split(" ")[0];
boolean whitelisted = Config.COMMAND_WHITELIST.PREFIXES.stream()
.anyMatch(prefix -> command.startsWith(prefix.toLowerCase()));
if (!whitelisted) {
event.setCancelled(true);
player.sendRichMessage(Messages.GENERIC.COMMAND_BLOCKED);
}
}
}
@@ -4,22 +4,47 @@ import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.services.RoundService;
import lombok.RequiredArgsConstructor;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@RequiredArgsConstructor
public class ItemSpawnListener implements Listener {
private final RoundService roundService;
private final Set<UUID> deathDrops = new HashSet<>();
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onItemSpawn(ItemSpawnEvent event) {
if (deathDrops.remove(event.getEntity().getUniqueId())) {
event.getEntity().setTicksLived(5400); // 300 seconds total - 5400 ticks = 30 seconds left
return;
}
setDespawnDelay(event.getEntity());
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onPlayerDeath(EntityDeathEvent event) {
if (!(event.getEntity() instanceof Player)) {
return;
}
ROUND_STATE roundState = roundService.getRoundState();
if (roundState == ROUND_STATE.SAFE_PHASE || roundState == ROUND_STATE.KILL_PHASE || roundState == ROUND_STATE.FINALE) {
event.getDrops().forEach(itemStack -> {
Item item = event.getEntity().getWorld().dropItemNaturally(event.getEntity().getLocation(), itemStack);
deathDrops.add(item.getUniqueId());
});
}
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerDropItem(PlayerDropItemEvent event) {
setDespawnDelay(event.getItemDrop());
@@ -7,6 +7,7 @@ import com.alttd.hunger_games.services.StatService;
import lombok.RequiredArgsConstructor;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
@@ -39,13 +40,11 @@ public class PlayerDamageListener implements Listener {
}
}
@EventHandler
@EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerDeath(PlayerDeathEvent event) {
Player player = event.getPlayer();
event.setShowDeathMessages(false);
event.deathMessage(null);
event.getDrops().clear();
player.getInventory().clear();
playerService.handlePlayerDeath(player);
statService.addDeath(player.getUniqueId());
@@ -1,18 +1,33 @@
package com.alttd.hunger_games.event_listeners;
import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.TabListService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandException;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
@Slf4j
@RequiredArgsConstructor
public class PlayerJoinListener implements Listener {
private final PlayerService playerService;
private final TabListService tabListService;
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
playerService.handleJoin(event.getPlayer());
Player player = event.getPlayer();
playerService.handleJoin(player);
tabListService.updatePlayerColor(player);
try {
Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
String.format("lp user %s permission settemp cmi.kit.hunger-games true 7d", player.getName()));
} catch (CommandException e) {
log.error("Failed to set participant permission for {}", player.getName(), e);
}
}
}
@@ -5,8 +5,11 @@ import com.alttd.hunger_games.utils.WorldBorderUtil;
import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import java.time.Duration;
@UtilityClass
public class GameStageHandler {
@@ -18,6 +21,10 @@ public class GameStageHandler {
broadcast(message);
}
public static void broadcastMessage(String message) {
broadcast(message);
}
public static void handleCountdownEnd() {
broadcast(Messages.GAME.STARTED);
}
@@ -29,5 +36,8 @@ public class GameStageHandler {
private static void broadcast(String message) {
Component component = MiniMessage.miniMessage().deserialize(message);
Bukkit.broadcast(component);
Title.Times times = Title.Times.times(Duration.ofSeconds(1), Duration.ofSeconds(5), Duration.ofSeconds(1));
Title title = Title.title(Component.empty(), component, times);
Bukkit.getOnlinePlayers().forEach(player -> player.showTitle(title));
}
}
@@ -42,9 +42,14 @@ public class BossBarService implements RoundListener {
public void updateCountdown(Duration timeLeft, Duration totalTime, ROUND_STATE state) {
String message = switch (state) {
case COUNTDOWN -> Messages.GAME.BOSSBAR_COUNTDOWN;
case SAFE_PHASE -> Messages.GAME.BOSSBAR_SAFE_PHASE;
case KILL_PHASE -> Messages.GAME.BOSSBAR_KILL_PHASE;
default -> throw new IllegalArgumentException("Invalid round state: " + state);
case SAFE_PHASE -> Messages.GAME.BOSSBAR_PVP_COUNTDOWN;
case KILL_PHASE -> Messages.GAME.BOSSBAR_SAFE_PHASE;
default -> {
if (state.ordinal() > ROUND_STATE.KILL_PHASE.ordinal()) {
yield Messages.GAME.BOSSBAR_KILL_PHASE;
}
throw new IllegalArgumentException("Invalid round state: " + state);
}
};
if (message.isEmpty()) {
@@ -61,6 +66,10 @@ public class BossBarService implements RoundListener {
}
private void updateBossBarColor(ROUND_STATE state) {
if (state == ROUND_STATE.ENDED || state == ROUND_STATE.PLAYER_REGISTRATION) {
hideAll();
return;
}
switch (state) {
case COUNTDOWN -> bossBar.color(BossBar.Color.YELLOW);
case SAFE_PHASE -> bossBar.color(BossBar.Color.GREEN);
@@ -87,27 +96,20 @@ public class BossBarService implements RoundListener {
}
private void updateViewers() {
Set<UUID> registeredPlayers = roundService.getPlayers(PLAYER_STATE.REGISTERED);
// Add new viewers
registeredPlayers.stream()
.filter(viewers::add)
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.forEach(player -> player.showBossBar(bossBar));
// Remove old viewers
Set<UUID> toRemove = viewers.stream()
.filter(uuid -> !registeredPlayers.contains(uuid))
.collect(Collectors.toSet());
for (UUID uuid : toRemove) {
viewers.remove(uuid);
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
player.hideBossBar(bossBar);
}
if (roundService.getRoundState() == ROUND_STATE.ENDED || roundService.getRoundState() == ROUND_STATE.PLAYER_REGISTRATION) {
hideAll();
return;
}
Bukkit.getOnlinePlayers().forEach(player -> {
if (viewers.add(player.getUniqueId())) {
player.showBossBar(bossBar);
}
});
viewers.removeIf(uuid -> {
Player player = Bukkit.getPlayer(uuid);
return player == null || !player.isOnline();
});
}
private void hideAll() {
@@ -1,6 +1,7 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.LootItems;
import com.alttd.hunger_games.data_objects.LootItem;
import com.alttd.hunger_games.data_objects.RARITY;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
@@ -38,15 +39,20 @@ public class LootService {
for (int i = 0; i < itemCount; i++) {
int slot = random.nextInt(27);
RARITY rarity = decideRarity();
List<Material> possibleItems = LootItems.ITEMS.get(rarity);
List<LootItem> possibleItems = LootItems.ITEMS.get(rarity);
if (possibleItems == null || possibleItems.isEmpty()) {
log.warn("No items found for rarity: {}", rarity);
continue;
}
Material material = possibleItems.get(random.nextInt(possibleItems.size()));
inventory.setItem(slot, new ItemStack(material));
LootItem lootItem = possibleItems.get(random.nextInt(possibleItems.size()));
if (lootItem == null || lootItem.material() == null) {
log.warn("LootItem or material is null for rarity: {}", rarity);
continue;
}
int amount = lootItem.maxAmount() > 1 ? random.nextInt(lootItem.maxAmount()) + 1 : 1;
inventory.setItem(slot, new ItemStack(lootItem.material(), amount));
}
return inventory;
@@ -4,6 +4,7 @@ import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.DESTINATION;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
@@ -13,6 +14,7 @@ import org.bukkit.event.player.PlayerRespawnEvent;
import java.util.*;
@Slf4j
public class PlayerService implements RoundListener {
private static PlayerService instance = null;
@@ -58,7 +60,7 @@ public class PlayerService implements RoundListener {
}
case ENDED -> {
roundService.clear();
Bukkit.getOnlinePlayers().forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.START_AREA));
Bukkit.getOnlinePlayers().forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA));
}
}
}
@@ -89,6 +91,7 @@ public class PlayerService implements RoundListener {
}
public void handleDisconnect(Player player) {
player.playerListName(null);
UUID uuid = player.getUniqueId();
roundService.setPlayerState(uuid, PLAYER_STATE.DISCONNECTED);
}
@@ -111,9 +114,17 @@ public class PlayerService implements RoundListener {
roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING);
int remaining = roundService.getPlayers(PLAYER_STATE.REGISTERED).size();
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_DEATH,
Placeholder.component("player", player.name()),
Placeholder.unparsed("remaining", String.valueOf(remaining))));
Player killer = player.getKiller();
if (killer != null) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_KILLED,
Placeholder.component("victim", player.name()),
Placeholder.component("killer", killer.name()),
Placeholder.unparsed("remaining", String.valueOf(remaining))));
} else {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_DEATH,
Placeholder.component("player", player.name()),
Placeholder.unparsed("remaining", String.valueOf(remaining))));
}
}
public void handlePlayerRespawn(PlayerRespawnEvent event) {
@@ -129,13 +140,18 @@ public class PlayerService implements RoundListener {
if (optionalPlayerState.isPresent()) {
PLAYER_STATE playerState = optionalPlayerState.get();
return Optional.of(switch (playerState) {
case DISCONNECTED, SPECTATING -> PLAYER_STATE.SPECTATING;
case DISCONNECTED, SPECTATING -> {
if (roundState.equals(ROUND_STATE.ENDED)) {
yield PLAYER_STATE.REGISTERED;
}
yield PLAYER_STATE.SPECTATING;
}
case REGISTERED -> PLAYER_STATE.REGISTERED;
});
}
return switch (roundState) {
case PLAYER_REGISTRATION, COUNTDOWN -> Optional.of(PLAYER_STATE.REGISTERED);
case KILL_PHASE, FINALE, ENDED, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING);
case PLAYER_REGISTRATION, COUNTDOWN, ENDED -> Optional.of(PLAYER_STATE.REGISTERED);
case KILL_PHASE, FINALE, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING);
};
}
}
@@ -31,8 +31,12 @@ public class PlayerTeleporterService implements RoundListener {
}
public void teleportPlayer(Player player, DESTINATION destination) {
log.info("Teleporting {} to {}}", player.getName(), destination.name());
player.teleport(getLocationFromDestination(destination));
Location locationFromDestination = getLocationFromDestination(destination);
try {
player.teleportAsync(getLocationFromDestination(destination));
} catch (Exception e) {
log.error("Failed to teleport player {} to destination {} ({})", player.getName(), destination.name(), locationFromDestination, e);
}
}
public void reset() {
@@ -1,8 +1,8 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.GameStage;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.game.GameStageHandler;
import lombok.Setter;
@@ -77,7 +77,7 @@ public class Round {
}
public void startRound() {
if (!roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
if (!roundState.equals(ROUND_STATE.PLAYER_REGISTRATION) && !roundState.equals(ROUND_STATE.ENDED)) {
throw new IllegalStateException("Round can not be started before player registration.");
}
roundState = ROUND_STATE.COUNTDOWN;
@@ -87,8 +87,12 @@ public class Round {
startCountdown(warmupDuration, () -> {
GameStageHandler.handleCountdownEnd();
nextStage();
scheduleNextStage(0);
nextStage(); // COUNTDOWN -> SAFE_PHASE
startCountdown(Config.ROUND.SAFE_PHASE, () -> {
nextStage(); // SAFE_PHASE -> KILL_PHASE
GameStageHandler.broadcastMessage(Messages.GAME.NEXT_STATE.replace("<round>", roundState.getHumanReadableName()));
scheduleNextStage(0);
});
});
}
@@ -120,13 +124,11 @@ public class Round {
private void scheduleNextStage(int index) {
List<GameStage> gameStageList = Config.ROUND.STAGES;
if (gameStageList.isEmpty()) {
nextStage(); // Transition to SAFE_PHASE
nextStage(); // Transition to KILL_PHASE
return;
}
if (gameStageList.size() <= index) {
nextStage();
startCountdown(Config.ROUND.TIME_TO_FINALE, this::nextStage);
return;
}
@@ -134,17 +136,8 @@ public class Round {
Duration duration = gameStage.getDuration();
startCountdown(duration, () -> {
if (gameStageList.size() <= index + 1) {
nextStage();
GameStageHandler.handleStageChange(gameStage.getWorldBorderSize());
startCountdown(Duration.ofMinutes(5), this::nextStage);
} else {
if (index == 0 && roundState.equals(ROUND_STATE.SAFE_PHASE)) {
nextStage();
}
GameStageHandler.handleStageChange(gameStage.getWorldBorderSize());
scheduleNextStage(index + 1);
}
GameStageHandler.handleStageChange(gameStage.getWorldBorderSize());
scheduleNextStage(index + 1);
});
}
@@ -153,16 +146,7 @@ public class Round {
return;
}
countdownFuture.cancel(true);
if (roundState.equals(ROUND_STATE.KILL_PHASE)) {
onEnd.run();
} else {
nextStage();
if (roundState.equals(ROUND_STATE.SAFE_PHASE)) {
scheduleNextStage(0);
} else if (roundState.equals(ROUND_STATE.KILL_PHASE)) {
scheduleNextStage(0);
}
}
onEnd.run();
}
}
@@ -1,9 +1,9 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.Main;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.DESTINATION;
import com.alttd.hunger_games.data_objects.GameStage;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.event_listeners.PlayerMovementListener;
@@ -15,6 +15,8 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.GameMode;
import org.bukkit.World;
import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
@@ -38,6 +40,8 @@ public class RoundService implements RoundListener {
private final Round round;
@Setter
private BossBarService bossBarService;
@Setter
private TabListService tabListService;
public static RoundService createSingletonInstance(Round round, Main main, LootService lootService, StatService statService, PlayerTeleporterService playerTeleporterService) {
if (instance != null) {
@@ -73,6 +77,13 @@ public class RoundService implements RoundListener {
}
}
if (tabListService != null) {
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
tabListService.updatePlayerColor(player);
}
}
if (!roundState.equals(ROUND_STATE.KILL_PHASE) && !roundState.equals(ROUND_STATE.FINALE)) {
return;
}
@@ -117,7 +128,10 @@ public class RoundService implements RoundListener {
public void stateChange(ROUND_STATE roundState) {
this.roundState = roundState;
stopFreeze();
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION) || roundState.equals(ROUND_STATE.ENDED)) {
if (roundState.equals(ROUND_STATE.ENDED)) {
cleanupItems();
}
clear();
lootService.clear();
}
@@ -127,7 +141,7 @@ public class RoundService implements RoundListener {
.forEach(player -> {
player.setHealth(20);
player.setFoodLevel(20);
player.setGameMode(GameMode.SURVIVAL);
player.setGameMode(GameMode.ADVENTURE);
player.getInventory().clear();
player.setTotalExperience(0);
player.setExp(0.0f);
@@ -137,7 +151,7 @@ public class RoundService implements RoundListener {
WorldBorderUtil.resetBorder();
}
if (roundState.equals(ROUND_STATE.FINALE)) {
WorldBorderUtil.setSize(60);
WorldBorderUtil.setSize(Config.ROUND.FINALE_BORDER_SIZE, 1);
}
if (!roundState.equals(ROUND_STATE.ENDED) && !roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.NEXT_STATE, Placeholder.parsed("round", roundState.getHumanReadableName())));
@@ -159,4 +173,13 @@ public class RoundService implements RoundListener {
main.getServer().getPluginManager().registerEvents(playerMovementListener, main);
}
private void cleanupItems() {
World world = Bukkit.getWorld(Config.ROUND.HG_WORLD);
if (world == null) {
log.error("Unable to cleanup items, world {} not found", Config.ROUND.HG_WORLD);
return;
}
world.getEntitiesByClass(Item.class).forEach(Item::remove);
}
}
@@ -45,8 +45,10 @@ public class StatService implements RoundListener {
@Override
public void stateChange(ROUND_STATE roundState) {
if (roundState == ROUND_STATE.PLAYER_REGISTRATION) {
if (roundState.equals(ROUND_STATE.ENDED)) {
saveRoundStats();
}
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
currentRoundStats.clear();
}
}
@@ -0,0 +1,46 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.util.UUID;
public class TabListService implements RoundListener {
private final RoundService roundService;
private ROUND_STATE roundState;
public TabListService(Round round, RoundService roundService) {
this.roundService = roundService;
this.roundState = round.register(this);
}
public void updatePlayerColor(Player player) {
if (!player.isOnline()) {
return;
}
if (roundState == null || roundState == ROUND_STATE.ENDED || roundState == ROUND_STATE.PLAYER_REGISTRATION) {
player.playerListName(null);
return;
}
PLAYER_STATE state = roundService.getPlayerState(player.getUniqueId()).orElse(PLAYER_STATE.SPECTATING);
switch (state) {
case REGISTERED -> player.playerListName(player.name().color(NamedTextColor.GREEN));
case SPECTATING, DISCONNECTED -> player.playerListName(player.name().color(NamedTextColor.RED));
}
}
public void updateAllPlayers() {
Bukkit.getOnlinePlayers().forEach(this::updatePlayerColor);
}
@Override
public void stateChange(ROUND_STATE roundState) {
this.roundState = roundState;
updateAllPlayers();
}
}
@@ -12,10 +12,8 @@ import java.util.Optional;
@UtilityClass
public class WorldBorderUtil {
private static final int BORDER_TRANSITION_SECONDS = 45;
public static void setSize(int size) {
setSize(size, BORDER_TRANSITION_SECONDS);
setSize(size, Config.ROUND.DEFAULT_BORDER_TRANSITION_SECONDS);
}
public static void setSize(int size, int seconds) {
+4
View File
@@ -3,6 +3,10 @@ version: ${version}
main: com.alttd.hunger_games.Main
api-version: "1.21"
# Load after Multiverse-Core to be able to find the teleport locations
softdepend:
- Multiverse-Core
commands:
hungergames:
description: Main HungerGames command