diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java index d8814e44..38362d12 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/GameWaitingLobby.java @@ -2,6 +2,7 @@ import eu.pb4.polymer.core.api.utils.PolymerUtils; import eu.pb4.sgui.api.GuiHelpers; +import eu.pb4.sgui.api.gui.GuiInterface; import net.minecraft.entity.boss.BossBar; import net.minecraft.screen.ScreenTexts; import net.minecraft.server.network.ServerPlayerEntity; @@ -23,14 +24,17 @@ import xyz.nucleoid.plasmid.api.game.event.GameWaitingLobbyEvents; import xyz.nucleoid.plasmid.api.game.player.JoinOffer; import xyz.nucleoid.plasmid.api.game.player.JoinOfferResult; +import xyz.nucleoid.plasmid.api.game.player.PlayerSet; import xyz.nucleoid.plasmid.api.game.rule.GameRuleType; import xyz.nucleoid.plasmid.api.game.common.widget.SidebarWidget; import xyz.nucleoid.plasmid.impl.game.common.ui.WaitingLobbyUi; import xyz.nucleoid.plasmid.impl.game.common.ui.element.LeaveGameWaitingLobbyUiElement; +import xyz.nucleoid.plasmid.impl.game.common.ui.element.ReadyGameWaitingLobbyUiElement; import xyz.nucleoid.plasmid.impl.game.manager.GameSpaceManagerImpl; import xyz.nucleoid.plasmid.impl.compatibility.AfkDisplayCompatibility; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; /** @@ -62,6 +66,8 @@ public final class GameWaitingLobby { private long countdownStart = -1; private long countdownDuration = -1; + private final ArrayList playerVotes = new ArrayList<>(); + private final HashMap playerToLayout = new HashMap<>(); private boolean startRequested; private boolean started; private List sidebarText; @@ -137,6 +143,16 @@ private Text onJoinMessage(ServerPlayerEntity player, Text currentText, Text def this.gameSpace.getServer().getCurrentPlayerCount() ), this.playerConfig.minPlayers()) - count; + if (count >= this.playerConfig.minPlayers()) { + this.showVoteReminder(); + for (ServerPlayerEntity plr : this.gameSpace.getPlayers().participants()) { + WaitingLobbyUiLayout layout = this.playerToLayout.get(plr); + if (layout != null) { + layout.refresh(); + } + } + } + return Text.empty() .append(currentText) .append(" ") @@ -186,7 +202,16 @@ public void setSidebarLines(List texts) { this.sidebarText.add(text); } } - + private void showVoteReminder() { + if (this.started) { + return; + } + PlayerSet participants = this.gameSpace.getPlayers().participants(); + MutableText styledText = Text.translatable("text.plasmid.game.waiting_lobby.player_vote_click_here", this.playerVotes.size(), String.format("%.0f", Math.ceil((double) participants.size() / 2))) + .styled((style -> style + .withColor(Formatting.BLUE))); + participants.sendActionBar(Text.translatable("text.plasmid.game.waiting_lobby.player_vote", this.playerConfig.minPlayers(), styledText)); + } private void onTick() { if (this.started) { return; @@ -221,6 +246,8 @@ private void onTick() { } } } + } else if (this.gameSpace.getPlayers().participants().size() >= this.playerConfig.minPlayers()) { + this.showVoteReminder(); } } @@ -252,10 +279,28 @@ private void onAddPlayer(ServerPlayerEntity player) { private void onRemovePlayer(ServerPlayerEntity player) { this.updateCountdown(); + if (this.playerVotes.remove(player) && this.gameSpace.getPlayers().participants().size() >= this.playerConfig.minPlayers()) { + this.showVoteReminder(); + } } private void onBuildUiLayout(WaitingLobbyUiLayout layout, ServerPlayerEntity player) { layout.addTrailing(new LeaveGameWaitingLobbyUiElement(this.gameSpace, player)); + layout.addTrailing(new ReadyGameWaitingLobbyUiElement(layout, this.gameSpace, this.playerConfig.minPlayers(), (hasVoted) -> { + if (hasVoted) { + this.playerVotes.add(player); + } else { + this.playerVotes.remove(player); + } + if (this.gameSpace.getPlayers().participants().size() >= this.playerConfig.minPlayers()) { + double playersNeededToStart = Math.ceil((double) this.gameSpace.getPlayers().participants().size() / 2); + if (playersNeededToStart <= this.playerVotes.size()) { + this.updateCountdown(); + } + this.showVoteReminder(); + } + })); + this.playerToLayout.put(player, layout); } private void updateCountdown() { @@ -293,7 +338,10 @@ private long getTargetCountdownDuration() { } if (this.gameSpace.getPlayers().participants().size() >= this.playerConfig.minPlayers()) { - if (this.isActiveFull(this.gameSpace.getPlayers().size())) { + double playersNeededToStart = Math.ceil((double) this.gameSpace.getPlayers().participants().size() / 2); + if (playersNeededToStart <= this.playerVotes.size()) { + return START_REQUESTED_COUNTDOWN; + } else if (this.isActiveFull(this.gameSpace.getPlayers().size())) { return countdown.fullSeconds() * 20L; } else if (this.isReady(this.gameSpace.getPlayers().participants().size())) { return countdown.readySeconds() * 20L; diff --git a/src/main/java/xyz/nucleoid/plasmid/api/game/common/config/WaitingLobbyConfig.java b/src/main/java/xyz/nucleoid/plasmid/api/game/common/config/WaitingLobbyConfig.java index 0f63f3fd..1dfa760a 100644 --- a/src/main/java/xyz/nucleoid/plasmid/api/game/common/config/WaitingLobbyConfig.java +++ b/src/main/java/xyz/nucleoid/plasmid/api/game/common/config/WaitingLobbyConfig.java @@ -3,6 +3,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.SharedConstants; import xyz.nucleoid.plasmid.api.game.common.GameWaitingLobby; import java.util.Optional; @@ -15,12 +16,13 @@ * * @see GameWaitingLobby */ -public record WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int minPlayers, int thresholdPlayers, Countdown countdown) { +public record WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int minPlayers, int thresholdPlayers, int playerVoteTimer, Countdown countdown) { public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec(instance -> { return instance.group( PlayerLimiterConfig.MAP_CODEC.forGetter(WaitingLobbyConfig::playerConfig), Codec.intRange(1, Integer.MAX_VALUE).fieldOf("min").forGetter(WaitingLobbyConfig::minPlayers), Codec.intRange(1, Integer.MAX_VALUE).optionalFieldOf("threshold").forGetter(c -> Optional.of(c.thresholdPlayers)), + Codec.intRange(1, Integer.MAX_VALUE).optionalFieldOf("player_vote_timer", 2 * SharedConstants.TICKS_PER_MINUTE).forGetter(WaitingLobbyConfig::playerVoteTimer), Countdown.CODEC.optionalFieldOf("countdown", Countdown.DEFAULT).forGetter(WaitingLobbyConfig::countdown) ).apply(instance, WaitingLobbyConfig::new); }); @@ -29,14 +31,18 @@ public record WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int minPlayer public WaitingLobbyConfig(int min, int max) { - this(new PlayerLimiterConfig(max), min, min, Countdown.DEFAULT); + this(new PlayerLimiterConfig(max), min, min, 2 * SharedConstants.TICKS_PER_MINUTE, Countdown.DEFAULT); } @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - private WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int min, Optional threshold, Countdown countdown) { - this(playerConfig, min, threshold.orElse(min), countdown); + private WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int min, Optional threshold, int playerVoteTimer, Countdown countdown) { + this(playerConfig, min, threshold.orElse(min), playerVoteTimer, countdown); } + // necessary to not break old implementations + public WaitingLobbyConfig(PlayerLimiterConfig playerConfig, int min, int threshold, Countdown countdown) { + this(playerConfig, min, threshold, 2 * SharedConstants.TICKS_PER_MINUTE, countdown); + } public record Countdown(int readySeconds, int fullSeconds) { public static final Countdown DEFAULT = new Countdown(30, 5); diff --git a/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/ReadyGameWaitingLobbyUiElement.java b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/ReadyGameWaitingLobbyUiElement.java new file mode 100644 index 00000000..bf483adf --- /dev/null +++ b/src/main/java/xyz/nucleoid/plasmid/impl/game/common/ui/element/ReadyGameWaitingLobbyUiElement.java @@ -0,0 +1,43 @@ +package xyz.nucleoid.plasmid.impl.game.common.ui.element; + +import eu.pb4.sgui.api.elements.GuiElementBuilder; +import eu.pb4.sgui.api.elements.GuiElementInterface; +import net.minecraft.item.Items; +import net.minecraft.text.Text; +import xyz.nucleoid.plasmid.api.game.GameSpace; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiElement; +import xyz.nucleoid.plasmid.api.game.common.ui.WaitingLobbyUiLayout; + +import java.util.function.Consumer; + +public class ReadyGameWaitingLobbyUiElement implements WaitingLobbyUiElement { + private final WaitingLobbyUiLayout layout; + private final Consumer callback; + private final GameSpace gameSpace; + private final int minPlayers; + public boolean hasVoted = false; + public ReadyGameWaitingLobbyUiElement(WaitingLobbyUiLayout layout, GameSpace gameSpace, int minPlayers, Consumer callback) { + this.layout = layout; + this.gameSpace = gameSpace; + this.minPlayers = minPlayers; + this.callback = callback; + } + + @Override + public GuiElementInterface createMainElement() { + if (this.gameSpace.getPlayers().participants().size() < this.minPlayers) { + + return new GuiElementBuilder(Items.AIR).build(); + } + return new GuiElementBuilder(!this.hasVoted ? Items.GREEN_WOOL : Items.RED_WOOL ) + .setItemName(Text.translatable("text.plasmid.game.waiting_lobby.player_vote_item")) + .setCallback((index, type, action, gui) -> { + if (WaitingLobbyUiElement.isClick(type, gui)) { + this.hasVoted = !this.hasVoted; + this.callback.accept(this.hasVoted); + this.layout.refresh(); + } + }) + .build(); + } +} diff --git a/src/main/resources/data/plasmid/lang/en_us.json b/src/main/resources/data/plasmid/lang/en_us.json index 571939d3..2b283539 100644 --- a/src/main/resources/data/plasmid/lang/en_us.json +++ b/src/main/resources/data/plasmid/lang/en_us.json @@ -64,6 +64,9 @@ "text.plasmid.game.waiting_lobby.leave_game": "Leave Game", "text.plasmid.game.waiting_lobby.sidebar.players": "Players: %s%s%s", "text.plasmid.game.waiting_lobby.players_needed_to_start": "%s more player(s) is needed to start!", + "text.plasmid.game.waiting_lobby.player_vote": "This game recommends %d player(s). %s", + "text.plasmid.game.waiting_lobby.player_vote_item": "Vote Start", + "text.plasmid.game.waiting_lobby.player_vote_click_here": "Would you like to start? (%s/%s players)", "text.plasmid.join_result.already_joined": "You have already joined this game!", "text.plasmid.join_result.error": "An unexpected error occurred while adding you to this game!", "text.plasmid.join_result.generic_error": "You cannot join this game!",