Compare commits

..
42 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
stijn a137ef4cc6 Handle empty stage list in Round and adjust state transitions in RoundService 2026-06-28 02:19:28 +02:00
stijn 62c9661c82 Adjust finale border size to 60 in RoundService 2026-06-28 00:35:39 +02:00
stijn 936cabfd42 Update boss bar messages to reflect accurate border shrinking phases and align config keys 2026-06-28 00:32:32 +02:00
stijn 38f8ffd4a3 Prioritize finale center when searching for safe locations in /stuck command 2026-06-27 23:58:54 +02:00
stijn 1a9f4cbd2d Refactor respawn handling: streamline spectator teleportation, reset player experience, adjust finale border size, and improve game stage countdown logic. 2026-06-27 23:57:07 +02:00
stijn 99bff5446d Adjust Round logic to handle last game stage countdown gracefully 2026-06-27 23:00:13 +02:00
stijn f9e00b04cc Update BORDER_TRANSITION_SECONDS in WorldBorderUtil to 45 2026-06-27 23:00:05 +02:00
stijn a7ce376abb Add ItemSpawnListener to control item despawn delays based on round phases 2026-06-27 22:55:09 +02:00
stijn 897c8ab14d Refactor world border handling: centralize logic in WorldBorderUtil and integrate into RoundService and GameStageHandler. Add logging for player teleportation and improve player reset process. 2026-06-27 22:32:43 +02:00
stijn a3085ad4b4 Prevent duplicate game end announcements and update RoundService initialization to include PlayerTeleporterService. 2026-06-27 22:17:50 +02:00
stijn 29c46ba485 Fix typo in getHumanReadableName across commands and services 2026-06-27 22:04:17 +02:00
stijn ca7b6f1152 Enhance death handling and round logic: suppress death messages, clear player inventories, reset health and food levels, and teleport players to spectator area or prepare them for countdown phase. Integrate PlayerTeleporterService into RoundService. 2026-06-27 22:02:05 +02:00
stijn 4bf6ca9dc4 Improve RoundState command to handle null ROUND_STATE values gracefully 2026-06-22 02:26:37 +02:00
stijn f0d3a20283 Add /hg nextphase and /hg end commands to manage game phases and rounds 2026-06-22 02:22:02 +02:00
stijn 233cffd2df Implement BossBarService for countdowns and finale updates, integrating dynamic boss bar messages and real-time player progress tracking. 2026-06-22 02:02:42 +02:00
stijn dd060bce48 Adjust teleportation logic to set correct direction for player-facing location 2026-06-21 23:56:16 +02:00
stijn 10a6103f32 Add LootItems configuration support and reload handling 2026-06-21 23:56:12 +02:00
stijn 646b9a2a35 Refactor STATS_FORMAT to use text block for improved readability 2026-06-15 23:29:28 +02:00
stijn 09b48fbafe Teleport spectating players to designated spectator area 2026-06-15 23:29:24 +02:00
stijn 6cd5315d28 Broadcast player death messages with remaining player count and suppress default death messages 2026-06-15 23:26:04 +02:00
stijn cf011ae72f Add winner declaration message and endRound logic with proper message broadcasting and error handling 2026-06-15 23:17:47 +02:00
stijn c8e85f186a Add player stats tracking with StatService and /hg stats command integration 2026-06-15 22:56:27 +02:00
32 changed files with 1229 additions and 109 deletions
+20 -13
View File
@@ -2,16 +2,10 @@ package com.alttd.hunger_games;
import com.alttd.hunger_games.commands.BaseCommand; import com.alttd.hunger_games.commands.BaseCommand;
import com.alttd.hunger_games.config.Config; 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.config.Messages;
import com.alttd.hunger_games.event_listeners.ChestListener; import com.alttd.hunger_games.event_listeners.*;
import com.alttd.hunger_games.event_listeners.PlayerDamageListener; import com.alttd.hunger_games.services.*;
import com.alttd.hunger_games.event_listeners.PlayerDisconnectListener;
import com.alttd.hunger_games.event_listeners.PlayerJoinListener;
import com.alttd.hunger_games.services.LootService;
import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.PlayerTeleporterService;
import com.alttd.hunger_games.services.Round;
import com.alttd.hunger_games.services.RoundService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.plugin.PluginManager; import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@@ -24,6 +18,9 @@ public final class Main extends JavaPlugin {
private PlayerService playerService; private PlayerService playerService;
private PlayerTeleporterService playerTeleporterService; private PlayerTeleporterService playerTeleporterService;
private LootService lootService; private LootService lootService;
private StatService statService;
private BossBarService bossBarService;
private TabListService tabListService;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -38,9 +35,16 @@ public final class Main extends JavaPlugin {
private void registerServices() { private void registerServices() {
round = Round.createSingletonInstance(); round = Round.createSingletonInstance();
lootService = new LootService(); lootService = new LootService();
roundService = RoundService.createSingletonInstance(round, this, lootService); statService = new StatService(this, round);
playerTeleporterService = PlayerTeleporterService.createSingletonInstance(); 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); playerService = PlayerService.createSingletonInstance(round, roundService, playerTeleporterService);
bossBarService = BossBarService.createSingletonInstance(round, roundService);
round.setBossBarService(bossBarService);
roundService.setBossBarService(bossBarService);
} }
@Override @Override
@@ -49,20 +53,23 @@ public final class Main extends JavaPlugin {
} }
private void registerCommands() { private void registerCommands() {
BaseCommand command = new BaseCommand(this, roundService, playerService, round); BaseCommand command = new BaseCommand(this, roundService, playerService, round, statService, tabListService);
} }
private void registerEvents() { private void registerEvents() {
PluginManager pluginManager = getServer().getPluginManager(); PluginManager pluginManager = getServer().getPluginManager();
pluginManager.registerEvents(new PlayerDisconnectListener(playerService), this); 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), this); pluginManager.registerEvents(new PlayerDamageListener(roundService, playerService, statService), this);
pluginManager.registerEvents(new ChestListener(roundService, lootService), this); pluginManager.registerEvents(new ChestListener(roundService, lootService), this);
pluginManager.registerEvents(new ItemSpawnListener(roundService), this);
pluginManager.registerEvents(new CommandListener(), this);
} }
public void reloadConfigs() { public void reloadConfigs() {
Config.reload(); Config.reload();
Messages.reload(); Messages.reload();
LootItems.reload();
} }
private void registerSchedulers() { private void registerSchedulers() {
@@ -3,9 +3,7 @@ package com.alttd.hunger_games.commands;
import com.alttd.hunger_games.Main; import com.alttd.hunger_games.Main;
import com.alttd.hunger_games.commands.subcommands.*; import com.alttd.hunger_games.commands.subcommands.*;
import com.alttd.hunger_games.config.Messages; import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.services.PlayerService; import com.alttd.hunger_games.services.*;
import com.alttd.hunger_games.services.Round;
import com.alttd.hunger_games.services.RoundService;
import lombok.Getter; import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -22,7 +20,7 @@ import java.util.stream.Collectors;
public class BaseCommand implements CommandExecutor, TabExecutor { public class BaseCommand implements CommandExecutor, TabExecutor {
private final List<SubCommand> subCommands; private final List<SubCommand> subCommands;
public BaseCommand(Main main, RoundService roundService, PlayerService playerService, Round round) { public BaseCommand(Main main, RoundService roundService, PlayerService playerService, Round round, StatService statService, TabListService tablistService) {
PluginCommand command = main.getCommand("hungergames"); PluginCommand command = main.getCommand("hungergames");
if (command == null) { if (command == null) {
subCommands = null; subCommands = null;
@@ -36,9 +34,13 @@ public class BaseCommand implements CommandExecutor, TabExecutor {
subCommands = new ArrayList<>(List.of( subCommands = new ArrayList<>(List.of(
new Reload(main), new Reload(main),
new RoundState(roundService), new RoundState(roundService),
new Register(playerService), new Register(playerService, tablistService),
new StartRound(round, roundService), new StartRound(round, roundService, playerService),
new Stuck(main) new NextPhase(round),
new EndRound(round),
new Stuck(main),
new Stats(statService),
new ListPlayers(roundService)
)); ));
} }
@@ -0,0 +1,37 @@
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.services.Round;
import lombok.RequiredArgsConstructor;
import org.bukkit.command.CommandSender;
import java.util.List;
@RequiredArgsConstructor
public class EndRound extends SubCommand {
private final Round round;
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
round.endRound();
commandSender.sendRichMessage("<green>Ending the round.</green>");
return true;
}
@Override
public String getName() {
return "end";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.END_ROUND;
}
}
@@ -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;
}
}
@@ -0,0 +1,37 @@
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.services.Round;
import lombok.RequiredArgsConstructor;
import org.bukkit.command.CommandSender;
import java.util.List;
@RequiredArgsConstructor
public class NextPhase extends SubCommand {
private final Round round;
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
round.forceNextPhase();
commandSender.sendRichMessage(Messages.COMMANDS.NEXT_PHASE_SUCCESS);
return true;
}
@Override
public String getName() {
return "nextphase";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
return List.of();
}
@Override
public String getHelpMessage() {
return Messages.HELP.NEXT_PHASE;
}
}
@@ -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.config.Messages;
import com.alttd.hunger_games.data_objects.PLAYER_STATE; import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.services.PlayerService; import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.TabListService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
@@ -15,16 +17,14 @@ import javax.annotation.Nullable;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@RequiredArgsConstructor
@Slf4j @Slf4j
public class Register extends SubCommand { public class Register extends SubCommand {
private final static OnlinePlayerParser ONLINE_PLAYER_PARSER = new OnlinePlayerParser(); private final static OnlinePlayerParser ONLINE_PLAYER_PARSER = new OnlinePlayerParser();
private final static int PLAYER_ARG = 1; private final static int PLAYER_ARG = 1;
private final PlayerService playerService; private final PlayerService playerService;
private final TabListService tabListService;
public Register(PlayerService playerService) {
this.playerService = playerService;
}
@Override @Override
public boolean onCommand(CommandSender commandSender, String[] args) { public boolean onCommand(CommandSender commandSender, String[] args) {
@@ -44,6 +44,7 @@ public class Register extends SubCommand {
playerToRegister.sendRichMessage(Messages.REGISTER.FAILED_TO_REGISTER_YOU); playerToRegister.sendRichMessage(Messages.REGISTER.FAILED_TO_REGISTER_YOU);
} else { } else {
notifyRegisteredPlayer(playerToRegister, nullablePlayerState); notifyRegisteredPlayer(playerToRegister, nullablePlayerState);
tabListService.updatePlayerColor(playerToRegister);
} }
if (commandSender instanceof Player commandPlayer) { if (commandSender instanceof Player commandPlayer) {
@@ -2,6 +2,7 @@ package com.alttd.hunger_games.commands.subcommands;
import com.alttd.hunger_games.commands.SubCommand; import com.alttd.hunger_games.commands.SubCommand;
import com.alttd.hunger_games.config.Messages; import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.services.RoundService; import com.alttd.hunger_games.services.RoundService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
@@ -16,9 +17,10 @@ public class RoundState extends SubCommand {
@Override @Override
public boolean onCommand(CommandSender commandSender, String[] args) { public boolean onCommand(CommandSender commandSender, String[] args) {
ROUND_STATE roundState = roundService.getRoundState();
commandSender.sendRichMessage(Messages.ROUND_STATE.ROUND_STATE, commandSender.sendRichMessage(Messages.ROUND_STATE.ROUND_STATE,
Placeholder.parsed("round_state", Placeholder.parsed("round_state",
roundService.getRoundState().getHumandReadableName())); roundState == null ? "null" : roundState.getHumanReadableName()));
return true; return true;
} }
@@ -3,10 +3,12 @@ package com.alttd.hunger_games.commands.subcommands;
import com.alttd.hunger_games.commands.SubCommand; import com.alttd.hunger_games.commands.SubCommand;
import com.alttd.hunger_games.config.Messages; import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.ROUND_STATE; 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.Round;
import com.alttd.hunger_games.services.RoundService; import com.alttd.hunger_games.services.RoundService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import java.util.List; import java.util.List;
@@ -16,15 +18,19 @@ public class StartRound extends SubCommand {
private final Round round; private final Round round;
private final RoundService roundService; private final RoundService roundService;
private final PlayerService playerService;
@Override @Override
public boolean onCommand(CommandSender commandSender, String[] args) { public boolean onCommand(CommandSender commandSender, String[] args) {
ROUND_STATE roundState = roundService.getRoundState(); 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(); round.startRound();
} else { } else {
commandSender.sendRichMessage(Messages.START_ROUND.CAN_NOT_START_ROUND, commandSender.sendRichMessage(Messages.START_ROUND.CAN_NOT_START_ROUND,
Placeholder.parsed("round_state", roundState.getHumandReadableName())); Placeholder.parsed("round_state", roundState.getHumanReadableName()));
} }
return true; return true;
} }
@@ -0,0 +1,78 @@
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.LifetimeStats;
import com.alttd.hunger_games.services.StatService;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class Stats extends SubCommand {
private final StatService statService;
public Stats(StatService statService) {
this.statService = statService;
}
@Override
public boolean onCommand(CommandSender commandSender, String[] args) {
OfflinePlayer target;
if (args.length < 2) {
if (!(commandSender instanceof Player player)) {
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_ONLY);
return true;
}
target = player;
} else {
target = Bukkit.getOfflinePlayerIfCached(args[1]);
if (target == null) {
target = Bukkit.getPlayer(args[1]);
}
if (target == null) {
commandSender.sendRichMessage(Messages.GENERIC.PLAYER_NOT_FOUND.replaceAll("<player>", args[1]));
return true;
}
}
LifetimeStats stats = statService.getOrCreateLifetimeStats(target.getUniqueId());
commandSender.sendRichMessage(Messages.STATS.STATS_FORMAT,
Placeholder.parsed("player", target.getName() != null ? target.getName() : "Unknown"),
Placeholder.parsed("kills", String.valueOf(stats.getTotalKills())),
Placeholder.parsed("deaths", String.valueOf(stats.getTotalDeaths())),
Placeholder.parsed("damage_dealt", String.format("%.2f", stats.getTotalDamageDealt())),
Placeholder.parsed("damage_taken", String.format("%.2f", stats.getTotalDamageTaken())),
Placeholder.parsed("wins", String.valueOf(stats.getTotalWins())),
Placeholder.parsed("rounds_played", String.valueOf(stats.getTotalRoundsPlayed())),
Placeholder.parsed("kd_ratio", String.format("%.2f", stats.getKDRatio()))
);
return true;
}
@Override
public String getName() {
return "stats";
}
@Override
public List<String> getTabComplete(CommandSender commandSender, String[] args) {
if (args.length == 2) {
return Bukkit.getOnlinePlayers().stream().map(Player::getName).collect(Collectors.toList());
}
return Collections.emptyList();
}
@Override
public String getHelpMessage() {
return Messages.HELP.STATS;
}
}
@@ -155,21 +155,31 @@ public class Stuck extends SubCommand implements Listener {
private Optional<Location> findSafeLocation(Location start) { private Optional<Location> findSafeLocation(Location start) {
int radius = 10; int radius = 10;
Location finaleCenter = Config.DESTINATION.FINALE_CENTER;
boolean prioritizeFinale = finaleCenter != null && finaleCenter.getWorld().equals(start.getWorld());
for (int r = 0; r <= radius; r++) { for (int r = 0; r <= radius; r++) {
List<Location> candidates = new ArrayList<>();
for (int x = -r; x <= r; x++) { for (int x = -r; x <= r; x++) {
for (int z = -r; z <= r; z++) { for (int z = -r; z <= r; z++) {
if (Math.abs(x) != r && Math.abs(z) != r) { if (Math.abs(x) != r && Math.abs(z) != r) {
continue; continue;
} }
candidates.add(start.clone().add(x, 0, z));
}
}
Location baseLocation = start.clone().add(x, 0, z); if (prioritizeFinale) {
candidates.sort(Comparator.comparingDouble(loc -> loc.distanceSquared(finaleCenter)));
}
for (Location baseLocation : candidates) {
Optional<Location> safeY = findSafeY(baseLocation); Optional<Location> safeY = findSafeY(baseLocation);
if (safeY.isPresent()) { if (safeY.isPresent()) {
return safeY; return safeY;
} }
} }
} }
}
return Optional.empty(); return Optional.empty();
} }
@@ -2,16 +2,12 @@ package com.alttd.hunger_games.config;
import com.alttd.hunger_games.data_objects.GameStage; import com.alttd.hunger_games.data_objects.GameStage;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.ConfigurationSection;
import java.io.File; import java.io.File;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Optional;
import java.util.Set;
@Slf4j @Slf4j
public class Config extends AbstractConfig { public class Config extends AbstractConfig {
@@ -44,8 +40,13 @@ public class Config extends AbstractConfig {
public static class ROUND { public static class ROUND {
private static final String prefix = "round."; private static final String prefix = "round.";
public static String HG_WORLD = "world";
public static Duration COUNTDOWN = Duration.ofSeconds(10); 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 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( public static List<GameStage> STAGES = List.of(
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(1000).build(), GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(1000).build(),
GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(750).build(), GameStage.builder().duration(Duration.ofMinutes(5)).worldBorderSize(750).build(),
@@ -54,9 +55,16 @@ public class Config extends AbstractConfig {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
HG_WORLD = config.getString(prefix, "hg-world", HG_WORLD);
int countdownSeconds = config.getInt(prefix, "countdown-seconds", Math.toIntExact(COUNTDOWN.toSeconds())); int countdownSeconds = config.getInt(prefix, "countdown-seconds", Math.toIntExact(COUNTDOWN.toSeconds()));
COUNTDOWN = Duration.ofSeconds(countdownSeconds); 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); 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"); ConfigurationSection configurationSection = config.getConfigurationSection(prefix + "stages");
if (configurationSection == null) { 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())); 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; package com.alttd.hunger_games.config;
import com.alttd.hunger_games.data_objects.LootItem;
import com.alttd.hunger_games.data_objects.RARITY; import com.alttd.hunger_games.data_objects.RARITY;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Material; import org.bukkit.Material;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@Slf4j
public class LootItems extends AbstractConfig { public class LootItems extends AbstractConfig {
static LootItems config; static LootItems config;
@@ -27,18 +31,40 @@ public class LootItems extends AbstractConfig {
public static class ITEMS { public static class ITEMS {
private static final String prefix = "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); return ITEMS.get(rarity);
} }
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
for (RARITY rarity : RARITY.values()) { for (RARITY rarity : RARITY.values()) {
List<String> materialNameList = ITEMS.get(rarity).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); 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);
} }
} }
} }
@@ -29,8 +29,12 @@ public class Messages extends AbstractConfig {
public static String ROUND_STATE = "<green>Show the current round state: <gold>/hg roundstate</gold></green>"; public static String ROUND_STATE = "<green>Show the current round state: <gold>/hg roundstate</gold></green>";
public static String REGISTER = "<green>Register a player for the game: <gold>/hg register <player></gold></green>"; 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 START_ROUND = "<green>Start the game: <gold>/hg start</gold></green>";
public static String NEXT_PHASE = "<green>Skip to the next phase/stage: <gold>/hg nextphase</gold></green>";
public static String RELOAD = "<green>Reload config and messages: <gold>/hg reload</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>"; 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") @SuppressWarnings("unused")
private static void load() { private static void load() {
@@ -39,8 +43,12 @@ public class Messages extends AbstractConfig {
ROUND_STATE = config.getString(prefix, "round-state", ROUND_STATE); ROUND_STATE = config.getString(prefix, "round-state", ROUND_STATE);
REGISTER = config.getString(prefix, "register", REGISTER); REGISTER = config.getString(prefix, "register", REGISTER);
START_ROUND = config.getString(prefix, "start", START_ROUND); START_ROUND = config.getString(prefix, "start", START_ROUND);
NEXT_PHASE = config.getString(prefix, "next-phase", NEXT_PHASE);
RELOAD = config.getString(prefix, "reload", RELOAD); RELOAD = config.getString(prefix, "reload", RELOAD);
STUCK = config.getString(prefix, "stuck", STUCK); 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);
} }
} }
@@ -51,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_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 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 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") @SuppressWarnings("unused")
private static void load() { private static void load() {
@@ -58,6 +67,7 @@ public class Messages extends AbstractConfig {
PLAYER_ONLY = config.getString(prefix, "player-only", PLAYER_ONLY); PLAYER_ONLY = config.getString(prefix, "player-only", PLAYER_ONLY);
PLAYER_NOT_FOUND = config.getString(prefix, "player-only", PLAYER_NOT_FOUND); PLAYER_NOT_FOUND = config.getString(prefix, "player-only", PLAYER_NOT_FOUND);
RELOAD_SUCCESS = config.getString(prefix, "reload-success", RELOAD_SUCCESS); RELOAD_SUCCESS = config.getString(prefix, "reload-success", RELOAD_SUCCESS);
COMMAND_BLOCKED = config.getString(prefix, "command-blocked", COMMAND_BLOCKED);
} }
} }
@@ -79,6 +89,17 @@ public class Messages extends AbstractConfig {
public static String STARTED = "<green><bold>The Hunger Games has begun! Good luck!</bold></green>"; public static String STARTED = "<green><bold>The Hunger Games has begun! Good luck!</bold></green>";
public static String BORDER_SHRINK = "<red>The border is shrinking to <size> blocks!</red>"; public static String BORDER_SHRINK = "<red>The border is shrinking to <size> blocks!</red>";
public static String CHEST_EMPTY = "<red>This chest is empty!</red>"; 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>";
public static String NEXT_STATE = "<gold>Starting <round>!</gold>";
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
@@ -86,6 +107,28 @@ public class Messages extends AbstractConfig {
STARTED = config.getString(prefix, "started", STARTED); STARTED = config.getString(prefix, "started", STARTED);
BORDER_SHRINK = config.getString(prefix, "border-shrink", BORDER_SHRINK); BORDER_SHRINK = config.getString(prefix, "border-shrink", BORDER_SHRINK);
CHEST_EMPTY = config.getString(prefix, "chest-empty", CHEST_EMPTY); 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);
NEXT_STATE = config.getString(prefix, "next-state", NEXT_STATE);
}
}
public static class START_ROUND {
private static final String prefix = "start-round.";
public static String CAN_NOT_START_ROUND = "<red>Unable to start the round, current state is <round_state></red>";
@SuppressWarnings("unused")
private static void load() {
CAN_NOT_START_ROUND = config.getString(prefix, "can-not-start", CAN_NOT_START_ROUND);
} }
} }
@@ -145,4 +188,39 @@ public class Messages extends AbstractConfig {
ALREADY_WARMING_UP = config.getString(prefix, "already-warming-up", ALREADY_WARMING_UP); ALREADY_WARMING_UP = config.getString(prefix, "already-warming-up", ALREADY_WARMING_UP);
} }
} }
public static class STATS {
private static final String prefix = "stats.";
public static String STATS_FORMAT = """
<gold>Stats for <player>:</gold>
<green>Kills: <gold><kills></gold></green>
<green>Deaths: <gold><deaths></gold></green>
<green>Damage Dealt: <gold><damage_dealt></gold></green>
<green>Damage Taken: <gold><damage_taken></gold></green>
<green>Wins: <gold><wins></gold></green>
<green>Rounds Played: <gold><rounds_played></gold></green>
<green>K/D Ratio: <gold><kd_ratio></gold></green>""";
@SuppressWarnings("unused")
private static void load() {
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,36 @@
package com.alttd.hunger_games.data_objects;
import lombok.Builder;
import lombok.Data;
import java.util.UUID;
@Data
@Builder
public class LifetimeStats {
private final UUID uuid;
private int totalKills;
private int totalDeaths;
private double totalDamageDealt;
private double totalDamageTaken;
private int totalWins;
private int totalRoundsPlayed;
public void addRoundStats(RoundStats roundStats) {
totalKills += roundStats.getKills();
totalDeaths += roundStats.getDeaths();
totalDamageDealt += roundStats.getDamageDealt();
totalDamageTaken += roundStats.getDamageTaken();
if (roundStats.isWon()) {
totalWins++;
}
totalRoundsPlayed++;
}
public double getKDRatio() {
if (totalDeaths == 0) {
return totalKills;
}
return (double) totalKills / totalDeaths;
}
}
@@ -0,0 +1,6 @@
package com.alttd.hunger_games.data_objects;
import org.bukkit.Material;
public record LootItem(Material material, int maxAmount) {
}
@@ -17,7 +17,7 @@ public enum ROUND_STATE {
return Optional.of(values()[ordinal() + 1]); return Optional.of(values()[ordinal() + 1]);
} }
public String getHumandReadableName() { public String getHumanReadableName() {
return switch (this) { return switch (this) {
case PLAYER_REGISTRATION -> "Player Registration"; case PLAYER_REGISTRATION -> "Player Registration";
case COUNTDOWN -> "Countdown"; case COUNTDOWN -> "Countdown";
@@ -0,0 +1,34 @@
package com.alttd.hunger_games.data_objects;
import lombok.Builder;
import lombok.Data;
import java.util.UUID;
@Data
@Builder
public class RoundStats {
private final UUID uuid;
private int kills;
private int deaths;
private double damageDealt;
private double damageTaken;
private int placement;
private boolean won;
public void addKill() {
kills++;
}
public void addDeath() {
deaths++;
}
public void addDamageDealt(double damage) {
damageDealt += damage;
}
public void addDamageTaken(double damage) {
damageTaken += damage;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,59 @@
package com.alttd.hunger_games.event_listeners;
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());
}
private void setDespawnDelay(Item item) {
ROUND_STATE roundState = roundService.getRoundState();
if (roundState == ROUND_STATE.SAFE_PHASE || roundState == ROUND_STATE.KILL_PHASE || roundState == ROUND_STATE.FINALE) {
item.setTicksLived(5800);
}
}
}
@@ -3,10 +3,13 @@ package com.alttd.hunger_games.event_listeners;
import com.alttd.hunger_games.data_objects.ROUND_STATE; import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.services.PlayerService; import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.RoundService; import com.alttd.hunger_games.services.RoundService;
import com.alttd.hunger_games.services.StatService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent; import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.player.PlayerRespawnEvent; import org.bukkit.event.player.PlayerRespawnEvent;
@@ -16,25 +19,43 @@ public class PlayerDamageListener implements Listener {
private final RoundService roundService; private final RoundService roundService;
private final PlayerService playerService; private final PlayerService playerService;
private final StatService statService;
@EventHandler @EventHandler
public void onEntityDamage(EntityDamageEvent event) { public void onEntityDamage(EntityDamageEvent event) {
if (!(event.getEntity() instanceof Player)) { if (!(event.getEntity() instanceof Player player)) {
return; return;
} }
ROUND_STATE roundState = roundService.getRoundState(); ROUND_STATE roundState = roundService.getRoundState();
if (roundState != ROUND_STATE.KILL_PHASE && roundState != ROUND_STATE.FINALE) { if (roundState != ROUND_STATE.KILL_PHASE && roundState != ROUND_STATE.FINALE) {
event.setCancelled(true); event.setCancelled(true);
return;
}
double damage = Math.min(event.getFinalDamage(), player.getHealth());
statService.addDamageTaken(player.getUniqueId(), damage);
if (event instanceof EntityDamageByEntityEvent damageByEntityEvent && damageByEntityEvent.getDamager() instanceof Player attacker) {
statService.addDamageDealt(attacker.getUniqueId(), damage);
} }
} }
@EventHandler @EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerDeath(PlayerDeathEvent event) { public void onPlayerDeath(PlayerDeathEvent event) {
playerService.handlePlayerDeath(event.getPlayer()); Player player = event.getPlayer();
event.setShowDeathMessages(false);
event.deathMessage(null);
playerService.handlePlayerDeath(player);
statService.addDeath(player.getUniqueId());
Player killer = player.getKiller();
if (killer != null) {
statService.addKill(killer.getUniqueId());
}
} }
@EventHandler @EventHandler
public void onPlayerRespawn(PlayerRespawnEvent event) { public void onPlayerRespawn(PlayerRespawnEvent event) {
playerService.handlePlayerRespawn(event.getPlayer()); playerService.handlePlayerRespawn(event);
} }
} }
@@ -1,18 +1,33 @@
package com.alttd.hunger_games.event_listeners; package com.alttd.hunger_games.event_listeners;
import com.alttd.hunger_games.services.PlayerService; import com.alttd.hunger_games.services.PlayerService;
import com.alttd.hunger_games.services.TabListService;
import lombok.RequiredArgsConstructor; 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.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerJoinEvent;
@Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class PlayerJoinListener implements Listener { public class PlayerJoinListener implements Listener {
private final PlayerService playerService; private final PlayerService playerService;
private final TabListService tabListService;
@EventHandler @EventHandler
public void onPlayerJoin(PlayerJoinEvent event) { 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);
}
} }
} }
@@ -1,60 +1,43 @@
package com.alttd.hunger_games.game; package com.alttd.hunger_games.game;
import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.config.Messages; import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.utils.WorldBorderUtil;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import java.util.Optional; import java.time.Duration;
@UtilityClass @UtilityClass
public class GameStageHandler { public class GameStageHandler {
private static final int BORDER_TRANSITION_SECONDS = 10;
public static void handleStageChange(int worldBorderSize) { public static void handleStageChange(int worldBorderSize) {
World world = getWorld().orElseThrow(() -> new IllegalStateException("No worlds available during state change,")); WorldBorderUtil.setSize(worldBorderSize);
WorldBorder border = world.getWorldBorder();
border.setSize(worldBorderSize, BORDER_TRANSITION_SECONDS);
String message = Messages.GAME.BORDER_SHRINK String message = Messages.GAME.BORDER_SHRINK
.replace("<size>", String.valueOf(worldBorderSize)); .replace("<size>", String.valueOf(worldBorderSize));
broadcast(message); broadcast(message);
} }
public static void broadcastMessage(String message) {
broadcast(message);
}
public static void handleCountdownEnd() { public static void handleCountdownEnd() {
broadcast(Messages.GAME.STARTED); broadcast(Messages.GAME.STARTED);
} }
public static void handleWarmup() { public static void handleWarmup() {
Location center = Config.DESTINATION.START_CENTER;
if (center == null || center.getWorld() == null) {
throw new IllegalStateException("Start center is not set during warmup.");
}
WorldBorder border = center.getWorld().getWorldBorder();
border.setCenter(center.getX(), center.getZ());
border.setSize(Config.ROUND.INITIAL_BORDER_SIZE);
broadcast(Messages.GAME.WARMUP); broadcast(Messages.GAME.WARMUP);
} }
private static Optional<World> getWorld() {
Location center = Config.DESTINATION.START_CENTER;
if (center != null && center.getWorld() != null) {
return Optional.ofNullable(center.getWorld());
}
return Bukkit.getWorlds().isEmpty() ? Optional.empty() : Optional.of(Bukkit.getWorlds().getFirst());
}
private static void broadcast(String message) { private static void broadcast(String message) {
Component component = MiniMessage.miniMessage().deserialize(message); Component component = MiniMessage.miniMessage().deserialize(message);
Bukkit.broadcast(component); 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));
} }
} }
@@ -0,0 +1,146 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import java.time.Duration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
public class BossBarService implements RoundListener {
private static BossBarService instance = null;
private final RoundService roundService;
private final BossBar bossBar;
private final Set<UUID> viewers = new HashSet<>();
private int maxPlayers = 0;
public BossBarService(Round round, RoundService roundService) {
this.roundService = roundService;
this.bossBar = BossBar.bossBar(Component.empty(), 1.0f, BossBar.Color.YELLOW, BossBar.Overlay.PROGRESS);
round.register(this);
}
public static BossBarService createSingletonInstance(Round round, RoundService roundService) {
if (instance != null) {
throw new IllegalStateException("BossBarService is already initialized.");
}
instance = new BossBarService(round, roundService);
return instance;
}
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_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()) {
bossBar.name(Component.empty());
bossBar.progress(0.0f);
return;
}
bossBar.name(MiniMessage.miniMessage().deserialize(message, Placeholder.unparsed("time", formatTime(timeLeft))));
bossBar.progress(Math.clamp((float) timeLeft.toMillis() / totalTime.toMillis(), 0.0f, 1.0f));
updateBossBarColor(state);
updateViewers();
}
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);
case KILL_PHASE -> bossBar.color(BossBar.Color.RED);
case FINALE -> bossBar.color(BossBar.Color.PURPLE);
default -> bossBar.color(BossBar.Color.WHITE);
}
}
public void updateFinale(int remainingPlayers) {
if (maxPlayers == 0) {
maxPlayers = remainingPlayers;
}
if (remainingPlayers <= 1) {
hideAll();
return;
}
bossBar.name(MiniMessage.miniMessage().deserialize(Messages.GAME.BOSSBAR_FINALE, Placeholder.unparsed("remaining", String.valueOf(remainingPlayers))));
bossBar.progress(Math.clamp((float) remainingPlayers / maxPlayers, 0.0f, 1.0f));
bossBar.color(BossBar.Color.PURPLE);
updateViewers();
}
private void updateViewers() {
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() {
viewers.stream()
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.forEach(player -> player.hideBossBar(bossBar));
viewers.clear();
}
private String formatTime(Duration duration) {
if (duration.isNegative()) {
return "0s";
}
long minutes = duration.toMinutesPart();
int seconds = duration.toSecondsPart();
if (minutes > 0) {
return "%dm %ds".formatted(minutes, seconds);
}
return "%ds".formatted(seconds);
}
@Override
public void stateChange(ROUND_STATE roundState) {
if (roundState == ROUND_STATE.PLAYER_REGISTRATION || roundState == ROUND_STATE.ENDED) {
hideAll();
maxPlayers = 0;
} else if (roundState == ROUND_STATE.FINALE) {
updateFinale(roundService.getPlayers(PLAYER_STATE.REGISTERED).size());
}
}
}
@@ -1,6 +1,7 @@
package com.alttd.hunger_games.services; package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.LootItems; import com.alttd.hunger_games.config.LootItems;
import com.alttd.hunger_games.data_objects.LootItem;
import com.alttd.hunger_games.data_objects.RARITY; import com.alttd.hunger_games.data_objects.RARITY;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
@@ -38,15 +39,20 @@ public class LootService {
for (int i = 0; i < itemCount; i++) { for (int i = 0; i < itemCount; i++) {
int slot = random.nextInt(27); int slot = random.nextInt(27);
RARITY rarity = decideRarity(); RARITY rarity = decideRarity();
List<Material> possibleItems = LootItems.ITEMS.get(rarity); List<LootItem> possibleItems = LootItems.ITEMS.get(rarity);
if (possibleItems == null || possibleItems.isEmpty()) { if (possibleItems == null || possibleItems.isEmpty()) {
log.warn("No items found for rarity: {}", rarity); log.warn("No items found for rarity: {}", rarity);
continue; continue;
} }
Material material = possibleItems.get(random.nextInt(possibleItems.size())); LootItem lootItem = possibleItems.get(random.nextInt(possibleItems.size()));
inventory.setItem(slot, new ItemStack(material)); 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; return inventory;
@@ -4,11 +4,17 @@ import com.alttd.hunger_games.config.Messages;
import com.alttd.hunger_games.data_objects.DESTINATION; import com.alttd.hunger_games.data_objects.DESTINATION;
import com.alttd.hunger_games.data_objects.PLAYER_STATE; import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_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; import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerRespawnEvent;
import java.util.*; import java.util.*;
@Slf4j
public class PlayerService implements RoundListener { public class PlayerService implements RoundListener {
private static PlayerService instance = null; private static PlayerService instance = null;
@@ -34,6 +40,7 @@ public class PlayerService implements RoundListener {
@Override @Override
public void stateChange(ROUND_STATE roundState) { public void stateChange(ROUND_STATE roundState) {
this.roundState = roundState; this.roundState = roundState;
playerTeleporterService.reset();
switch (roundState) { switch (roundState) {
case PLAYER_REGISTRATION, KILL_PHASE, SAFE_PHASE -> { case PLAYER_REGISTRATION, KILL_PHASE, SAFE_PHASE -> {
//Nothing //Nothing
@@ -53,7 +60,7 @@ public class PlayerService implements RoundListener {
} }
case ENDED -> { case ENDED -> {
roundService.clear(); roundService.clear();
Bukkit.getOnlinePlayers().forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.START_AREA)); Bukkit.getOnlinePlayers().forEach(player -> playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA));
} }
} }
} }
@@ -76,7 +83,7 @@ public class PlayerService implements RoundListener {
PLAYER_STATE playerState = optionalPlayerState.get(); PLAYER_STATE playerState = optionalPlayerState.get();
if (playerState == PLAYER_STATE.SPECTATING) { if (playerState == PLAYER_STATE.SPECTATING) {
//TODO: teleport player to spectator location playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA);
} }
roundService.setPlayerState(player.getUniqueId(), playerState); roundService.setPlayerState(player.getUniqueId(), playerState);
@@ -84,6 +91,7 @@ public class PlayerService implements RoundListener {
} }
public void handleDisconnect(Player player) { public void handleDisconnect(Player player) {
player.playerListName(null);
UUID uuid = player.getUniqueId(); UUID uuid = player.getUniqueId();
roundService.setPlayerState(uuid, PLAYER_STATE.DISCONNECTED); roundService.setPlayerState(uuid, PLAYER_STATE.DISCONNECTED);
} }
@@ -104,13 +112,24 @@ public class PlayerService implements RoundListener {
public void handlePlayerDeath(Player player) { public void handlePlayerDeath(Player player) {
UUID uuid = player.getUniqueId(); UUID uuid = player.getUniqueId();
roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING); roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING);
// TODO: send a message when a player is killed
int remaining = roundService.getPlayers(PLAYER_STATE.REGISTERED).size();
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(Player player) { public void handlePlayerRespawn(PlayerRespawnEvent event) {
roundService.getPlayerState(player.getUniqueId()) Location spectatorArea = playerTeleporterService.getLocationFromDestination(DESTINATION.SPECTATOR_AREA);
.filter(state -> state == PLAYER_STATE.SPECTATING) event.setRespawnLocation(spectatorArea);
.ifPresent(state -> playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA));
} }
private Optional<PLAYER_STATE> determinePlayerState(UUID playerUuid) { private Optional<PLAYER_STATE> determinePlayerState(UUID playerUuid) {
@@ -121,13 +140,18 @@ public class PlayerService implements RoundListener {
if (optionalPlayerState.isPresent()) { if (optionalPlayerState.isPresent()) {
PLAYER_STATE playerState = optionalPlayerState.get(); PLAYER_STATE playerState = optionalPlayerState.get();
return Optional.of(switch (playerState) { 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; case REGISTERED -> PLAYER_STATE.REGISTERED;
}); });
} }
return switch (roundState) { return switch (roundState) {
case PLAYER_REGISTRATION, COUNTDOWN -> Optional.of(PLAYER_STATE.REGISTERED); case PLAYER_REGISTRATION, COUNTDOWN, ENDED -> Optional.of(PLAYER_STATE.REGISTERED);
case KILL_PHASE, FINALE, ENDED, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING); case KILL_PHASE, FINALE, SAFE_PHASE -> Optional.of(PLAYER_STATE.SPECTATING);
}; };
} }
} }
@@ -4,6 +4,7 @@ import com.alttd.hunger_games.config.Config;
import com.alttd.hunger_games.data_objects.DESTINATION; import com.alttd.hunger_games.data_objects.DESTINATION;
import com.alttd.hunger_games.data_objects.ROUND_STATE; import com.alttd.hunger_games.data_objects.ROUND_STATE;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -12,6 +13,7 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@Slf4j
@NoArgsConstructor @NoArgsConstructor
public class PlayerTeleporterService implements RoundListener { public class PlayerTeleporterService implements RoundListener {
@@ -29,10 +31,20 @@ public class PlayerTeleporterService implements RoundListener {
} }
public void teleportPlayer(Player player, DESTINATION destination) { public void teleportPlayer(Player player, DESTINATION destination) {
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);
}
} }
private Location getLocationFromDestination(DESTINATION destination) { public void reset() {
placementCount.clear();
usedLocations.clear();
}
public Location getLocationFromDestination(DESTINATION destination) {
return switch (destination) { return switch (destination) {
case START_AREA -> { case START_AREA -> {
Location startCenter = Config.DESTINATION.START_CENTER; Location startCenter = Config.DESTINATION.START_CENTER;
@@ -66,7 +78,10 @@ public class PlayerTeleporterService implements RoundListener {
double angleRadians = Math.toRadians(angleDegrees); double angleRadians = Math.toRadians(angleDegrees);
double x = center.getX() + radius * Math.cos(angleRadians); double x = center.getX() + radius * Math.cos(angleRadians);
double z = center.getZ() + radius * Math.sin(angleRadians); double z = center.getZ() + radius * Math.sin(angleRadians);
Location location = new Location(center.getWorld(), x, center.getY(), z); Location location = new Location(center.getWorld(), x, center.getY(), z);
location.setDirection(center.toVector().subtract(location.toVector()));
usedLocations.computeIfAbsent(destination, k -> new HashSet<>()).add(location); usedLocations.computeIfAbsent(destination, k -> new HashSet<>()).add(location);
return location; return location;
} }
@@ -1,16 +1,20 @@
package com.alttd.hunger_games.services; package com.alttd.hunger_games.services;
import com.alttd.hunger_games.config.Config; 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.GameStage;
import com.alttd.hunger_games.data_objects.ROUND_STATE; import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.game.GameStageHandler; import com.alttd.hunger_games.game.GameStageHandler;
import lombok.Setter;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class Round { public class Round {
@@ -19,6 +23,10 @@ public class Round {
private final List<RoundListener> listeners = new ArrayList<>(); private final List<RoundListener> listeners = new ArrayList<>();
private ROUND_STATE roundState = ROUND_STATE.PLAYER_REGISTRATION; private ROUND_STATE roundState = ROUND_STATE.PLAYER_REGISTRATION;
private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
private ScheduledFuture<?> countdownFuture = null;
private Runnable onEnd = null;
@Setter
private BossBarService bossBarService;
private Round() { private Round() {
@@ -42,10 +50,21 @@ public class Round {
} }
public void stop() { public void stop() {
if (countdownFuture != null) {
countdownFuture.cancel(true);
}
roundState = ROUND_STATE.PLAYER_REGISTRATION; roundState = ROUND_STATE.PLAYER_REGISTRATION;
listeners.forEach(roundListener -> roundListener.stateChange(roundState)); listeners.forEach(roundListener -> roundListener.stateChange(roundState));
} }
public void endRound() {
if (countdownFuture != null) {
countdownFuture.cancel(true);
}
roundState = ROUND_STATE.ENDED;
listeners.forEach(roundListener -> roundListener.stateChange(roundState));
}
private void nextStage() { private void nextStage() {
Optional<ROUND_STATE> optionalNextRoundState = roundState.next(); Optional<ROUND_STATE> optionalNextRoundState = roundState.next();
if (optionalNextRoundState.isEmpty()) { if (optionalNextRoundState.isEmpty()) {
@@ -58,32 +77,76 @@ public class Round {
} }
public void startRound() { 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."); throw new IllegalStateException("Round can not be started before player registration.");
} }
roundState = ROUND_STATE.COUNTDOWN; roundState = ROUND_STATE.COUNTDOWN;
listeners.forEach(roundListener -> roundListener.stateChange(roundState)); listeners.forEach(roundListener -> roundListener.stateChange(roundState));
GameStageHandler.handleWarmup(); GameStageHandler.handleWarmup();
Duration warmupDuration = Config.ROUND.COUNTDOWN; Duration warmupDuration = Config.ROUND.COUNTDOWN;
scheduledExecutorService.schedule(() -> {
startCountdown(warmupDuration, () -> {
GameStageHandler.handleCountdownEnd(); GameStageHandler.handleCountdownEnd();
nextStage(); 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); scheduleNextStage(0);
}, warmupDuration.toSeconds(), TimeUnit.SECONDS); });
});
}
private void startCountdown(Duration duration, Runnable onEnd) {
if (countdownFuture != null) {
countdownFuture.cancel(true);
}
this.onEnd = onEnd;
final Instant endTime = Instant.now().plus(duration);
countdownFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
if (roundState.equals(ROUND_STATE.FINALE)) {
if (countdownFuture != null) {
countdownFuture.cancel(false);
countdownFuture = null;
}
return;
}
Duration timeLeft = Duration.between(Instant.now(), endTime);
if (bossBarService != null) {
bossBarService.updateCountdown(timeLeft, duration, roundState);
}
if (timeLeft.isZero() || timeLeft.isNegative()) {
onEnd.run();
}
}, 0, 1, TimeUnit.SECONDS);
} }
private void scheduleNextStage(int index) { private void scheduleNextStage(int index) {
List<GameStage> gameStageList = Config.ROUND.STAGES; List<GameStage> gameStageList = Config.ROUND.STAGES;
if (gameStageList.isEmpty()) {
return;
}
if (gameStageList.size() <= index) { if (gameStageList.size() <= index) {
nextStage(); startCountdown(Config.ROUND.TIME_TO_FINALE, this::nextStage);
return; return;
} }
GameStage gameStage = gameStageList.get(index); GameStage gameStage = gameStageList.get(index);
Duration duration = gameStage.getDuration(); Duration duration = gameStage.getDuration();
scheduledExecutorService.schedule(() -> {
startCountdown(duration, () -> {
GameStageHandler.handleStageChange(gameStage.getWorldBorderSize()); GameStageHandler.handleStageChange(gameStage.getWorldBorderSize());
scheduleNextStage(index + 1); scheduleNextStage(index + 1);
}, duration.toSeconds(), TimeUnit.SECONDS); });
} }
public void forceNextPhase() {
if (countdownFuture == null || onEnd == null) {
return;
}
countdownFuture.cancel(true);
onEnd.run();
}
} }
@@ -1,40 +1,63 @@
package com.alttd.hunger_games.services; package com.alttd.hunger_games.services;
import com.alttd.hunger_games.Main; 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.PLAYER_STATE; import com.alttd.hunger_games.data_objects.PLAYER_STATE;
import com.alttd.hunger_games.data_objects.ROUND_STATE; import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.event_listeners.PlayerMovementListener; import com.alttd.hunger_games.event_listeners.PlayerMovementListener;
import com.alttd.hunger_games.utils.WorldBorderUtil;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
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; 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.entity.Player;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j
public class RoundService implements RoundListener { public class RoundService implements RoundListener {
private static RoundService instance = null; private static RoundService instance = null;
private PlayerMovementListener playerMovementListener; private final PlayerTeleporterService playerTeleporterService;
private final Main main; private final Main main;
private final LootService lootService; private final LootService lootService;
private final StatService statService;
private PlayerMovementListener playerMovementListener;
@Getter @Getter
private ROUND_STATE roundState; private ROUND_STATE roundState;
private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>(); private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>();
public static RoundService createSingletonInstance(Round round, Main main, LootService lootService) { 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) { if (instance != null) {
throw new IllegalStateException("RoundService is already initialized."); throw new IllegalStateException("RoundService is already initialized.");
} }
instance = new RoundService(round, main, lootService); instance = new RoundService(round, main, lootService, statService, playerTeleporterService);
return instance; return instance;
} }
private RoundService(Round round, Main main, LootService lootService) { private RoundService(Round round, Main main, LootService lootService, StatService statService, PlayerTeleporterService playerTeleporterService) {
this.round = round;
this.roundState = round.register(this); this.roundState = round.register(this);
this.main = main; this.main = main;
this.lootService = lootService; this.lootService = lootService;
this.statService = statService;
this.playerTeleporterService = playerTeleporterService;
} }
public Set<UUID> getPlayers(PLAYER_STATE playerState) { public Set<UUID> getPlayers(PLAYER_STATE playerState) {
@@ -45,7 +68,22 @@ public class RoundService implements RoundListener {
} }
protected void setPlayerState(UUID uuid, PLAYER_STATE playerState) { protected void setPlayerState(UUID uuid, PLAYER_STATE playerState) {
players.put(uuid, playerState); PLAYER_STATE oldState = players.put(uuid, playerState);
if (playerState == PLAYER_STATE.SPECTATING && oldState == PLAYER_STATE.REGISTERED) {
int remaining = getPlayers(PLAYER_STATE.REGISTERED).size();
statService.setPlacement(uuid, remaining + 1);
if (roundState == ROUND_STATE.FINALE && bossBarService != null) {
bossBarService.updateFinale(remaining);
}
}
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)) { if (!roundState.equals(ROUND_STATE.KILL_PHASE) && !roundState.equals(ROUND_STATE.FINALE)) {
return; return;
} }
@@ -58,8 +96,24 @@ public class RoundService implements RoundListener {
private void declareWinner(UUID winnerUUID) { private void declareWinner(UUID winnerUUID) {
Player player = Bukkit.getPlayer(winnerUUID); Player player = Bukkit.getPlayer(winnerUUID);
//TODO message if (player == null) {
//TODO reset round log.error("Winner {} not found", winnerUUID);
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<red>Winner not found</red>"));
return;
}
statService.setPlacement(winnerUUID, 1);
statService.setWon(winnerUUID, true);
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.WINNER, Placeholder.component("player", player.name())));
playerTeleporterService.teleportPlayer(player, DESTINATION.SPECTATOR_AREA);
player.setHealth(20);
player.setFoodLevel(20);
player.getInventory().clear();
player.setTotalExperience(0);
player.setExp(0.0f);
player.setLevel(0);
round.endRound();
} }
protected void clear() { protected void clear() {
@@ -74,12 +128,33 @@ public class RoundService implements RoundListener {
public void stateChange(ROUND_STATE roundState) { public void stateChange(ROUND_STATE roundState) {
this.roundState = roundState; this.roundState = roundState;
stopFreeze(); 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(); clear();
lootService.clear(); lootService.clear();
} }
if (roundState.equals(ROUND_STATE.COUNTDOWN)) { if (roundState.equals(ROUND_STATE.COUNTDOWN)) {
Bukkit.getOnlinePlayers().stream()
.filter(player -> PLAYER_STATE.REGISTERED.equals(getPlayerState(player.getUniqueId()).orElse(null)))
.forEach(player -> {
player.setHealth(20);
player.setFoodLevel(20);
player.setGameMode(GameMode.ADVENTURE);
player.getInventory().clear();
player.setTotalExperience(0);
player.setExp(0.0f);
player.setLevel(0);
});
startFreeze(); startFreeze();
WorldBorderUtil.resetBorder();
}
if (roundState.equals(ROUND_STATE.FINALE)) {
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())));
} }
} }
@@ -97,4 +172,14 @@ public class RoundService implements RoundListener {
playerMovementListener = new PlayerMovementListener(uuidSet); playerMovementListener = new PlayerMovementListener(uuidSet);
main.getServer().getPluginManager().registerEvents(playerMovementListener, main); 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);
}
} }
@@ -0,0 +1,123 @@
package com.alttd.hunger_games.services;
import com.alttd.hunger_games.Main;
import com.alttd.hunger_games.data_objects.LifetimeStats;
import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.data_objects.RoundStats;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class StatService implements RoundListener {
private final Main main;
private final Map<UUID, RoundStats> currentRoundStats = new ConcurrentHashMap<>();
private final Map<UUID, LifetimeStats> lifetimeStatsCache = new ConcurrentHashMap<>();
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final File statsDirectory;
private final File lifetimeStatsFile;
public StatService(Main main, Round round) {
this.main = main;
this.statsDirectory = new File(main.getDataFolder(), "stats");
if (!statsDirectory.exists()) {
statsDirectory.mkdirs();
}
this.lifetimeStatsFile = new File(statsDirectory, "lifetime.json");
round.register(this);
loadLifetimeStats();
}
public RoundStats getOrCreateRoundStats(UUID uuid) {
return currentRoundStats.computeIfAbsent(uuid, k -> RoundStats.builder().uuid(k).build());
}
public LifetimeStats getOrCreateLifetimeStats(UUID uuid) {
return lifetimeStatsCache.computeIfAbsent(uuid, k -> LifetimeStats.builder().uuid(k).build());
}
@Override
public void stateChange(ROUND_STATE roundState) {
if (roundState.equals(ROUND_STATE.ENDED)) {
saveRoundStats();
}
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION)) {
currentRoundStats.clear();
}
}
private void saveRoundStats() {
if (currentRoundStats.isEmpty()) {
return;
}
long timestamp = System.currentTimeMillis();
File roundFile = new File(statsDirectory, "round_" + timestamp + ".json");
try (FileWriter writer = new FileWriter(roundFile)) {
gson.toJson(currentRoundStats.values(), writer);
} catch (IOException e) {
main.getLogger().severe("Could not save round stats: " + e.getMessage());
}
for (RoundStats roundStats : currentRoundStats.values()) {
LifetimeStats lifetime = getOrCreateLifetimeStats(roundStats.getUuid());
lifetime.addRoundStats(roundStats);
}
saveLifetimeStats();
}
private void loadLifetimeStats() {
if (!lifetimeStatsFile.exists()) {
return;
}
try (FileReader reader = new FileReader(lifetimeStatsFile)) {
LifetimeStats[] stats = gson.fromJson(reader, LifetimeStats[].class);
if (stats != null) {
for (LifetimeStats s : stats) {
lifetimeStatsCache.put(s.getUuid(), s);
}
}
} catch (IOException e) {
main.getLogger().severe("Could not load lifetime stats: " + e.getMessage());
}
}
private void saveLifetimeStats() {
try (FileWriter writer = new FileWriter(lifetimeStatsFile)) {
gson.toJson(lifetimeStatsCache.values(), writer);
} catch (IOException e) {
main.getLogger().severe("Could not save lifetime stats: " + e.getMessage());
}
}
public void addKill(UUID uuid) {
getOrCreateRoundStats(uuid).addKill();
}
public void addDeath(UUID uuid) {
getOrCreateRoundStats(uuid).addDeath();
}
public void addDamageDealt(UUID uuid, double damage) {
getOrCreateRoundStats(uuid).addDamageDealt(damage);
}
public void addDamageTaken(UUID uuid, double damage) {
getOrCreateRoundStats(uuid).addDamageTaken(damage);
}
public void setPlacement(UUID uuid, int placement) {
getOrCreateRoundStats(uuid).setPlacement(placement);
}
public void setWon(UUID uuid, boolean won) {
getOrCreateRoundStats(uuid).setWon(won);
}
}
@@ -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();
}
}
@@ -0,0 +1,44 @@
package com.alttd.hunger_games.utils;
import com.alttd.hunger_games.config.Config;
import lombok.experimental.UtilityClass;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.WorldBorder;
import java.util.Optional;
@UtilityClass
public class WorldBorderUtil {
public static void setSize(int size) {
setSize(size, Config.ROUND.DEFAULT_BORDER_TRANSITION_SECONDS);
}
public static void setSize(int size, int seconds) {
getWorld().ifPresent(world -> {
WorldBorder border = world.getWorldBorder();
border.setSize(size, seconds);
});
}
public static void resetBorder() {
Location center = Config.DESTINATION.START_CENTER;
if (center == null || center.getWorld() == null) {
return;
}
WorldBorder border = center.getWorld().getWorldBorder();
border.setCenter(center.getX(), center.getZ());
border.setSize(Config.ROUND.INITIAL_BORDER_SIZE);
}
private static Optional<World> getWorld() {
Location center = Config.DESTINATION.START_CENTER;
if (center != null && center.getWorld() != null) {
return Optional.ofNullable(center.getWorld());
}
return Bukkit.getWorlds().isEmpty() ? Optional.empty() : Optional.of(Bukkit.getWorlds().getFirst());
}
}
+4
View File
@@ -3,6 +3,10 @@ version: ${version}
main: com.alttd.hunger_games.Main main: com.alttd.hunger_games.Main
api-version: "1.21" api-version: "1.21"
# Load after Multiverse-Core to be able to find the teleport locations
softdepend:
- Multiverse-Core
commands: commands:
hungergames: hungergames:
description: Main HungerGames command description: Main HungerGames command