Implement BossBarService for countdowns and finale updates, integrating dynamic boss bar messages and real-time player progress tracking.

This commit is contained in:
akastijn 2026-06-22 02:02:42 +02:00
parent dd060bce48
commit 233cffd2df
6 changed files with 206 additions and 11 deletions

View File

@ -8,12 +8,7 @@ import com.alttd.hunger_games.event_listeners.ChestListener;
import com.alttd.hunger_games.event_listeners.PlayerDamageListener; import com.alttd.hunger_games.event_listeners.PlayerDamageListener;
import com.alttd.hunger_games.event_listeners.PlayerDisconnectListener; import com.alttd.hunger_games.event_listeners.PlayerDisconnectListener;
import com.alttd.hunger_games.event_listeners.PlayerJoinListener; import com.alttd.hunger_games.event_listeners.PlayerJoinListener;
import com.alttd.hunger_games.services.LootService; import com.alttd.hunger_games.services.*;
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 com.alttd.hunger_games.services.StatService;
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;
@ -27,6 +22,7 @@ public final class Main extends JavaPlugin {
private PlayerTeleporterService playerTeleporterService; private PlayerTeleporterService playerTeleporterService;
private LootService lootService; private LootService lootService;
private StatService statService; private StatService statService;
private BossBarService bossBarService;
@Override @Override
public void onEnable() { public void onEnable() {
@ -45,6 +41,10 @@ public final class Main extends JavaPlugin {
roundService = RoundService.createSingletonInstance(round, this, lootService, statService); roundService = RoundService.createSingletonInstance(round, this, lootService, statService);
playerTeleporterService = PlayerTeleporterService.createSingletonInstance(); playerTeleporterService = PlayerTeleporterService.createSingletonInstance();
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

View File

@ -36,7 +36,7 @@ public class LootItems extends AbstractConfig {
@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<String> materialNameList = ITEMS.getOrDefault(rarity, List.of()).stream().map(Material::name).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()); ITEMS.put(rarity, materialList.stream().map(Material::getMaterial).toList());
} }

View File

@ -84,6 +84,11 @@ public class Messages extends AbstractConfig {
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 BOSSBAR_COUNTDOWN = "<gold>Loot phase in <time></gold>";
public static String BOSSBAR_SAFE_PHASE = "<green>Kill phase in <time></green>";
public static String BOSSBAR_KILL_PHASE = "<red>Border shrink in <time></red>";
public static String BOSSBAR_FINALE = "<gold>Remaining Players: <remaining></gold>";
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static void load() { private static void load() {
WARMUP = config.getString(prefix, "warmup", WARMUP); WARMUP = config.getString(prefix, "warmup", WARMUP);
@ -92,6 +97,11 @@ 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);
BOSSBAR_COUNTDOWN = config.getString(prefix, "bossbar-countdown", BOSSBAR_COUNTDOWN);
BOSSBAR_SAFE_PHASE = config.getString(prefix, "bossbar-safe-phase", BOSSBAR_SAFE_PHASE);
BOSSBAR_KILL_PHASE = config.getString(prefix, "bossbar-kill-phase", BOSSBAR_KILL_PHASE);
BOSSBAR_FINALE = config.getString(prefix, "bossbar-finale", BOSSBAR_FINALE);
} }
} }

View File

@ -0,0 +1,144 @@
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_SAFE_PHASE;
case KILL_PHASE -> Messages.GAME.BOSSBAR_KILL_PHASE;
default -> 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) {
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() {
Set<UUID> registeredPlayers = roundService.getPlayers(PLAYER_STATE.REGISTERED);
// Add new viewers
registeredPlayers.stream()
.filter(viewers::add)
.map(Bukkit::getPlayer)
.filter(Objects::nonNull)
.forEach(player -> player.showBossBar(bossBar));
// Remove old viewers
Set<UUID> toRemove = viewers.stream()
.filter(uuid -> !registeredPlayers.contains(uuid))
.collect(Collectors.toSet());
for (UUID uuid : toRemove) {
viewers.remove(uuid);
Player player = Bukkit.getPlayer(uuid);
if (player != null) {
player.hideBossBar(bossBar);
}
}
}
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());
}
}
}

View File

@ -6,11 +6,13 @@ import com.alttd.hunger_games.data_objects.ROUND_STATE;
import com.alttd.hunger_games.game.GameStageHandler; import com.alttd.hunger_games.game.GameStageHandler;
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 +21,8 @@ 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 BossBarService bossBarService;
private Round() { private Round() {
@ -42,11 +46,17 @@ 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() { public void endRound() {
if (countdownFuture != null) {
countdownFuture.cancel(true);
}
roundState = ROUND_STATE.ENDED; roundState = ROUND_STATE.ENDED;
listeners.forEach(roundListener -> roundListener.stateChange(roundState)); listeners.forEach(roundListener -> roundListener.stateChange(roundState));
} }
@ -70,11 +80,29 @@ public class Round {
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();
scheduleNextStage(0); scheduleNextStage(0);
}, warmupDuration.toSeconds(), TimeUnit.SECONDS); });
}
private void startCountdown(Duration duration, Runnable onEnd) {
if (countdownFuture != null) {
countdownFuture.cancel(true);
}
final Instant endTime = Instant.now().plus(duration);
countdownFuture = scheduledExecutorService.scheduleAtFixedRate(() -> {
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) {
@ -86,9 +114,14 @@ public class Round {
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 setBossBarService(BossBarService bossBarService) {
this.bossBarService = bossBarService;
} }
} }

View File

@ -30,6 +30,7 @@ public class RoundService implements RoundListener {
private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>(); private final HashMap<UUID, PLAYER_STATE> players = new HashMap<>();
private final Round round; private final Round round;
private BossBarService bossBarService;
public static RoundService createSingletonInstance(Round round, Main main, LootService lootService, StatService statService) { public static RoundService createSingletonInstance(Round round, Main main, LootService lootService, StatService statService) {
if (instance != null) { if (instance != null) {
@ -59,6 +60,9 @@ public class RoundService implements RoundListener {
if (playerState == PLAYER_STATE.SPECTATING && oldState == PLAYER_STATE.REGISTERED) { if (playerState == PLAYER_STATE.SPECTATING && oldState == PLAYER_STATE.REGISTERED) {
int remaining = getPlayers(PLAYER_STATE.REGISTERED).size(); int remaining = getPlayers(PLAYER_STATE.REGISTERED).size();
statService.setPlacement(uuid, remaining + 1); statService.setPlacement(uuid, remaining + 1);
if (roundState == ROUND_STATE.FINALE && bossBarService != null) {
bossBarService.updateFinale(remaining);
}
} }
if (!roundState.equals(ROUND_STATE.KILL_PHASE) && !roundState.equals(ROUND_STATE.FINALE)) { if (!roundState.equals(ROUND_STATE.KILL_PHASE) && !roundState.equals(ROUND_STATE.FINALE)) {
@ -121,4 +125,8 @@ 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);
} }
public void setBossBarService(BossBarService bossBarService) {
this.bossBarService = bossBarService;
}
} }