show participant status in tab using colors

announce killer if present
show everyone the bossbar
drop items on death
give players participation price permission
This commit is contained in:
2026-07-11 22:13:52 +02:00
parent fc7b5c76a6
commit 416c44a8f3
12 changed files with 147 additions and 38 deletions
@@ -20,6 +20,7 @@ public final class Main extends JavaPlugin {
private LootService lootService; private LootService lootService;
private StatService statService; private StatService statService;
private BossBarService bossBarService; private BossBarService bossBarService;
private TabListService tabListService;
@Override @Override
public void onEnable() { public void onEnable() {
@@ -42,6 +43,8 @@ public final class Main extends JavaPlugin {
bossBarService = BossBarService.createSingletonInstance(round, roundService); bossBarService = BossBarService.createSingletonInstance(round, roundService);
round.setBossBarService(bossBarService); round.setBossBarService(bossBarService);
roundService.setBossBarService(bossBarService); roundService.setBossBarService(bossBarService);
tabListService = new TabListService(round, roundService);
} }
@Override @Override
@@ -50,13 +53,13 @@ public final class Main extends JavaPlugin {
} }
private void registerCommands() { 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() { 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, statService), 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 ItemSpawnListener(roundService), this);
@@ -3,10 +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 com.alttd.hunger_games.services.StatService;
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;
@@ -23,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, StatService statService) { 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;
@@ -37,7 +34,7 @@ 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, playerService), new StartRound(round, roundService, playerService),
new NextPhase(round), new NextPhase(round),
new EndRound(round), new EndRound(round),
@@ -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) {
@@ -40,6 +40,7 @@ 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 SAFE_PHASE = Duration.ofSeconds(30);
public static Duration TIME_TO_FINALE = Duration.ofMinutes(5); public static Duration TIME_TO_FINALE = Duration.ofMinutes(5);
@@ -54,6 +55,7 @@ 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())); int safePhaseSeconds = config.getInt(prefix, "safe-phase-seconds", Math.toIntExact(SAFE_PHASE.toSeconds()));
@@ -89,6 +89,7 @@ public class Messages extends AbstractConfig {
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 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_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_COUNTDOWN = "<gold>Loot phase in <time></gold>";
public static String BOSSBAR_PVP_COUNTDOWN = "<green>PVP starts in <time></green>"; public static String BOSSBAR_PVP_COUNTDOWN = "<green>PVP starts in <time></green>";
@@ -106,6 +107,7 @@ public class Messages extends AbstractConfig {
CHEST_EMPTY = config.getString(prefix, "chest-empty", CHEST_EMPTY); CHEST_EMPTY = config.getString(prefix, "chest-empty", CHEST_EMPTY);
WINNER = config.getString(prefix, "winner", WINNER); WINNER = config.getString(prefix, "winner", WINNER);
PLAYER_DEATH = config.getString(prefix, "player-death", PLAYER_DEATH); 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_COUNTDOWN = config.getString(prefix, "bossbar-countdown", BOSSBAR_COUNTDOWN);
BOSSBAR_PVP_COUNTDOWN = config.getString(prefix, "bossbar-pvp-countdown", BOSSBAR_PVP_COUNTDOWN); BOSSBAR_PVP_COUNTDOWN = config.getString(prefix, "bossbar-pvp-countdown", BOSSBAR_PVP_COUNTDOWN);
@@ -4,22 +4,47 @@ 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 org.bukkit.entity.Item; import org.bukkit.entity.Item;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority; import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.ItemSpawnEvent; import org.bukkit.event.entity.ItemSpawnEvent;
import org.bukkit.event.player.PlayerDropItemEvent; import org.bukkit.event.player.PlayerDropItemEvent;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@RequiredArgsConstructor @RequiredArgsConstructor
public class ItemSpawnListener implements Listener { public class ItemSpawnListener implements Listener {
private final RoundService roundService; private final RoundService roundService;
private final Set<UUID> deathDrops = new HashSet<>();
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onItemSpawn(ItemSpawnEvent event) { 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()); 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) @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerDropItem(PlayerDropItemEvent event) { public void onPlayerDropItem(PlayerDropItemEvent event) {
setDespawnDelay(event.getItemDrop()); setDespawnDelay(event.getItemDrop());
@@ -45,8 +45,6 @@ public class PlayerDamageListener implements Listener {
Player player = event.getPlayer(); Player player = event.getPlayer();
event.setShowDeathMessages(false); event.setShowDeathMessages(false);
event.deathMessage(null); event.deathMessage(null);
event.getDrops().clear();
player.getInventory().clear();
playerService.handlePlayerDeath(player); playerService.handlePlayerDeath(player);
statService.addDeath(player.getUniqueId()); statService.addDeath(player.getUniqueId());
@@ -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);
}
} }
} }
@@ -66,6 +66,10 @@ public class BossBarService implements RoundListener {
} }
private void updateBossBarColor(ROUND_STATE state) { private void updateBossBarColor(ROUND_STATE state) {
if (state == ROUND_STATE.ENDED || state == ROUND_STATE.PLAYER_REGISTRATION) {
hideAll();
return;
}
switch (state) { switch (state) {
case COUNTDOWN -> bossBar.color(BossBar.Color.YELLOW); case COUNTDOWN -> bossBar.color(BossBar.Color.YELLOW);
case SAFE_PHASE -> bossBar.color(BossBar.Color.GREEN); case SAFE_PHASE -> bossBar.color(BossBar.Color.GREEN);
@@ -92,27 +96,20 @@ public class BossBarService implements RoundListener {
} }
private void updateViewers() { private void updateViewers() {
Set<UUID> registeredPlayers = roundService.getPlayers(PLAYER_STATE.REGISTERED); if (roundService.getRoundState() == ROUND_STATE.ENDED || roundService.getRoundState() == ROUND_STATE.PLAYER_REGISTRATION) {
hideAll();
// Add new viewers return;
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);
}
} }
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() { private void hideAll() {
@@ -91,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);
} }
@@ -113,9 +114,17 @@ public class PlayerService implements RoundListener {
roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING); roundService.setPlayerState(uuid, PLAYER_STATE.SPECTATING);
int remaining = roundService.getPlayers(PLAYER_STATE.REGISTERED).size(); int remaining = roundService.getPlayers(PLAYER_STATE.REGISTERED).size();
Bukkit.broadcast(MiniMessage.miniMessage().deserialize(Messages.GAME.PLAYER_DEATH, Player killer = player.getKiller();
Placeholder.component("player", player.name()), if (killer != null) {
Placeholder.unparsed("remaining", String.valueOf(remaining)))); 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) { public void handlePlayerRespawn(PlayerRespawnEvent event) {
@@ -15,6 +15,8 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
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;
import org.bukkit.GameMode; 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;
@@ -118,6 +120,9 @@ public class RoundService implements RoundListener {
this.roundState = roundState; this.roundState = roundState;
stopFreeze(); stopFreeze();
if (roundState.equals(ROUND_STATE.PLAYER_REGISTRATION) || roundState.equals(ROUND_STATE.ENDED)) { 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();
} }
@@ -159,4 +164,13 @@ public class RoundService implements RoundListener {
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,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();
}
}