diff --git a/game/src/main/java/net/onelitefeather/cygnus/phase/LobbyPhase.java b/game/src/main/java/net/onelitefeather/cygnus/phase/LobbyPhase.java index bcd9ae4..18b1882 100644 --- a/game/src/main/java/net/onelitefeather/cygnus/phase/LobbyPhase.java +++ b/game/src/main/java/net/onelitefeather/cygnus/phase/LobbyPhase.java @@ -1,17 +1,15 @@ package net.onelitefeather.cygnus.phase; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; +import net.minestom.server.network.ConnectionManager; import net.minestom.server.event.EventDispatcher; import net.onelitefeather.cygnus.common.config.GameConfig; import net.onelitefeather.cygnus.map.event.GameMapLoadEvent; import net.onelitefeather.cygnus.map.event.GamePrepareEvent; +import net.onelitefeather.cygnus.phase.task.LobbyWaitingTask; import net.theevilreaper.xerus.api.phase.TickDirection; import net.theevilreaper.xerus.api.phase.TimedPhase; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.format.NamedTextColor; -import net.minestom.server.MinecraftServer; -import net.minestom.server.entity.Player; -import net.minestom.server.network.ConnectionManager; -import net.minestom.server.timer.Task; import java.time.temporal.ChronoUnit; @@ -19,31 +17,31 @@ /** * Represents the lobby phase of the game. - * - *
During this phase the game waits until enough players joined to start the + *
+ * During this phase, the game waits until enough players have joined to start the * match countdown. The phase updates the player level and experience bar to - * visualize the remaining time until the game starts.
- * - *The phase can be paused automatically if the required player count is not - * reached anymore. It also supports force starting the game with a reduced - * countdown duration.
- * - *While the countdown is running, the game map loading and stamina - * initialization are triggered at specific countdown timestamps.
+ * visualize the remaining time until the game starts, and manages the action bar + * waiting display using a tick-aligned scheduler. + * * * @author theEvilReaper - * @version 1.0.0 + * @version 1.2.0 * @since 1.0.0 */ public final class LobbyPhase extends TimedPhase { private static final ConnectionManager CONNECTION_MANAGER = MinecraftServer.getConnectionManager(); - private final Task waitingTask; + private final int lobbyTime; private final int minPlayers; + private final LobbyWaitingTask waitingDisplay; private boolean forceStarted; - private Component displayComponent; + /** + * Constructs a new LobbyPhase. + * + * @param gameConfig the configuration settings for the game + */ public LobbyPhase(GameConfig gameConfig) { super("Lobby", ChronoUnit.SECONDS, 1); this.lobbyTime = gameConfig.lobbyTime(); @@ -51,39 +49,44 @@ public LobbyPhase(GameConfig gameConfig) { this.setPaused(true); this.setCurrentTicks(lobbyTime); this.setTickDirection(TickDirection.DOWN); - this.updateComponent(); - this.waitingTask = MinecraftServer.getSchedulerManager().buildTask(() -> { - if (isPaused() || isFinished() || CONNECTION_MANAGER.getOnlinePlayers().isEmpty()) - return; - CONNECTION_MANAGER.getOnlinePlayers().forEach( - player -> player.sendActionBar(this.displayComponent) - ); - }).repeat(30, ChronoUnit.SECONDS).schedule(); - } - private void updateComponent() { - int playerDifference = Math.max(0, this.minPlayers - CONNECTION_MANAGER.getOnlinePlayers().size()); - this.displayComponent = Component.text("Need", NamedTextColor.GRAY) - .append(Component.space()) - .append(Component.text(playerDifference, NamedTextColor.RED)) - .append(Component.space()) - .append(Component.text("players to start", NamedTextColor.GRAY)); + // Instantiate the waiting display only once during initialization + this.waitingDisplay = new LobbyWaitingTask(this.minPlayers); + this.waitingDisplay.update(CONNECTION_MANAGER.getOnlinePlayers().size()); } + /** + * Checks if the start condition is met. + * If the online player count reaches the minimum requirement, the countdown starts + * and the waiting action bar display is stopped. + */ public void checkStartCondition() { - this.updateComponent(); - if (isPaused() && CONNECTION_MANAGER.getOnlinePlayers().size() >= this.minPlayers) { + int onlineCount = CONNECTION_MANAGER.getOnlinePlayers().size(); + + if (isPaused() && onlineCount >= this.minPlayers) { this.setPaused(false); + this.waitingDisplay.stop(); // Stop the loop, but keep the final object + } else { + this.waitingDisplay.update(onlineCount); } } + /** + * Checks if the countdown should stop when players leave the server. + * Re-activates the waiting display if the player count drops below the requirement. + */ public void checkStopCondition() { - if (waitingTask.isAlive() && CONNECTION_MANAGER.getOnlinePlayers().size() - 1 <= this.minPlayers) { + int onlineCount = CONNECTION_MANAGER.getOnlinePlayers().size(); + + if (onlineCount - 1 < this.minPlayers) { this.setPaused(true); - this.updateComponent(); this.setCurrentTicks(this.lobbyTime); this.forceStarted = false; - setLevel(); + this.setLevel(); + + // Simply restart the existing display instead of allocating a new one + this.waitingDisplay.start(); + this.waitingDisplay.update(onlineCount); } } @@ -95,7 +98,7 @@ public void start() { @Override protected void onFinish() { - this.waitingTask.cancel(); + this.waitingDisplay.stop(); // Stop the tick-aligned loop } @Override @@ -113,14 +116,20 @@ public void onUpdate() { /** * Checks if the player requirements are met. - * If the player requirements are not met, the phase will be paused. + * Re-creates the display if the player count drops below the threshold. */ public void checkPlayerRequirements() { - if (CONNECTION_MANAGER.getOnlinePlayers().size() - 1 < this.minPlayers) { + int onlineCount = CONNECTION_MANAGER.getOnlinePlayers().size(); + + if (onlineCount - 1 < this.minPlayers) { this.setPaused(true); this.setForceStarted(false); this.setCurrentTicks(this.lobbyTime); this.setLevel(this.lobbyTime); + + // Simply restart the existing display instead of allocating a new one + this.waitingDisplay.start(); + this.waitingDisplay.update(onlineCount); } } @@ -129,11 +138,9 @@ private void setLevel() { } /** - * Sets the level for all online players. - * The level is calculated by the current ticks. - * The experience is calculated by the current ticks divided by the lobby phase time. + * Sets the level and XP progress for all online players. * - * @param amount the amount of the level + * @param amount the current countdown level */ private void setLevel(int amount) { if (amount < 0) return; @@ -146,29 +153,40 @@ private void setLevel(int amount) { } /** - * Sets the level for the given player. + * Sets the level for a specific player (typically called on join). + * Also immediately sends the waiting action bar if the lobby is paused. * - * @param player the player to set the level + * @param player the player to set the level for */ public void setLevel(Player player) { if (getCurrentTicks() < 0) return; player.setLevel(getCurrentTicks()); player.setExp(getCurrentTicks() / (float) this.lobbyTime); + + // Immediately send the action bar to the newly spawned player + if (isPaused()) { + this.waitingDisplay.send(player); + } } /** * Returns if the phase was force started. * - * @return True if the phase was force started otherwise false + * @return true if the phase was force started, otherwise false */ public boolean isForceStarted() { return forceStarted; } + /** + * Sets whether the phase is force started. + * + * @param forceStarted true to force start, otherwise false + */ public void setForceStarted(boolean forceStarted) { if (forceStarted) { this.setCurrentTicks(FORCE_START_TIME); } this.forceStarted = forceStarted; } -} +} \ No newline at end of file diff --git a/game/src/main/java/net/onelitefeather/cygnus/phase/task/LobbyWaitingTask.java b/game/src/main/java/net/onelitefeather/cygnus/phase/task/LobbyWaitingTask.java new file mode 100644 index 0000000..ad9945c --- /dev/null +++ b/game/src/main/java/net/onelitefeather/cygnus/phase/task/LobbyWaitingTask.java @@ -0,0 +1,132 @@ +package net.onelitefeather.cygnus.phase.task; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.MinecraftServer; +import net.minestom.server.entity.Player; + +import java.util.Collection; + +/** + * Handles the action bar display logic for players while the lobby is waiting + * for the minimum required player count to start the match countdown. + *+ * This class is designed to be reusable. The tick-aligned scheduler can be started + * and stopped dynamically without re-instantiating the object. + *
+ * + * @author theEvilReaper + * @version 1.3.0 + * @since 1.0.0 + **/ +public final class LobbyWaitingTask { + + private static final Component PREFIX_NEED = Component.text("Need", NamedTextColor.GRAY); + private static final Component SUFFIX_TO_START = Component.text("players to start", NamedTextColor.GRAY); + private static final Component SPACE = Component.space(); + private static final int DISPLAY_REFRESH_INTERVAL_TICKS = 40; // Approx. 2 seconds + + private final int minPlayers; + private final Component[] cachedComponents; + private final Runnable tickRunnable; + + private Component displayComponent = Component.empty(); + private int tickCounter = 0; + private boolean running = false; + + /** + * Creates a new instance of the waiting display and starts the tick-aligned loop. + * + * @param minPlayers the minimum number of players required to start the game + */ + public LobbyWaitingTask(int minPlayers) { + this.minPlayers = minPlayers; + this.cachedComponents = new Component[minPlayers + 1]; + + // Pre-build all possible text components for player differences [0 ... minPlayers] + for (int diff = 0; diff <= minPlayers; diff++) { + this.cachedComponents[diff] = PREFIX_NEED + .append(SPACE) + .append(Component.text(diff, NamedTextColor.RED)) + .append(SPACE) + .append(SUFFIX_TO_START); + } + + // Define the self-scheduling tick task + this.tickRunnable = new Runnable() { + @Override + public void run() { + if (!running) { + return; // Gracefully exit the loop when stopped + } + + // Send the action bar only at the specified tick interval + if (tickCounter++ % DISPLAY_REFRESH_INTERVAL_TICKS == 0) { + sendToAll(MinecraftServer.getConnectionManager().getOnlinePlayers()); + } + + // Schedule this task again for the next server tick + MinecraftServer.getSchedulerManager().scheduleNextTick(this); + } + }; + + // Start the loop automatically on creation + this.start(); + } + + /** + * Starts the tick-aligned display loop if it is not already running. + */ + public void start() { + if (this.running) { + return; // Prevent duplicate scheduler registrations + } + this.running = true; + this.tickCounter = 0; + MinecraftServer.getSchedulerManager().scheduleNextTick(this.tickRunnable); + } + + /** + * Stops the tick-aligned loop. + * The loop will automatically terminate on the next scheduled tick. + */ + public void stop() { + this.running = false; + } + + /** + * Updates the player count difference and immediately refreshes the action bar + * for all online players using pre-cached components. + * + * @param onlineCount the current number of online players + */ + public void update(int onlineCount) { + int playerDifference = Math.clamp(this.minPlayers - onlineCount, 0, this.minPlayers); + this.displayComponent = this.cachedComponents[playerDifference]; + + if (running) { + sendToAll(MinecraftServer.getConnectionManager().getOnlinePlayers()); + } + } + + /** + * Sends the current action bar message immediately to a specific player + * (e.g., when a player joins the server). + * + * @param player the player to send the message to + */ + public void send(Player player) { + if (running && displayComponent != Component.empty()) { + player.sendActionBar(this.displayComponent); + } + } + + private void sendToAll(Collection