diff --git a/common/src/main/java/io/github/gaming32/bingo/Bingo.java b/common/src/main/java/io/github/gaming32/bingo/Bingo.java index bd1b7f62..3e698393 100644 --- a/common/src/main/java/io/github/gaming32/bingo/Bingo.java +++ b/common/src/main/java/io/github/gaming32/bingo/Bingo.java @@ -21,6 +21,7 @@ import io.github.gaming32.bingo.network.messages.s2c.RemoveBoardPayload; import io.github.gaming32.bingo.network.messages.s2c.ResyncStatesPayload; import io.github.gaming32.bingo.network.messages.s2c.SyncTeamPayload; +import io.github.gaming32.bingo.network.messages.s2c.UpdateEndTimePayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateProgressPayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateStatePayload; import io.github.gaming32.bingo.platform.BingoPlatform; @@ -179,6 +180,7 @@ private static void registerPayloadHandlers() { registrar.register(PacketFlow.CLIENTBOUND, SyncTeamPayload.TYPE, SyncTeamPayload.CODEC); registrar.register(PacketFlow.CLIENTBOUND, UpdateProgressPayload.TYPE, UpdateProgressPayload.CODEC); registrar.register(PacketFlow.CLIENTBOUND, UpdateStatePayload.TYPE, UpdateStatePayload.CODEC); + registrar.register(PacketFlow.CLIENTBOUND, UpdateEndTimePayload.TYPE, UpdateEndTimePayload.CODEC); }); } diff --git a/common/src/main/java/io/github/gaming32/bingo/BingoCommand.java b/common/src/main/java/io/github/gaming32/bingo/BingoCommand.java index 1a798203..1b341d54 100644 --- a/common/src/main/java/io/github/gaming32/bingo/BingoCommand.java +++ b/common/src/main/java/io/github/gaming32/bingo/BingoCommand.java @@ -129,6 +129,10 @@ public class BingoCommand { private static final CommandSwitch SEED = CommandSwitch .argument("--seed", LongArgumentType.longArg()) .build(RandomSupport::generateUniqueSeed); + private static final CommandSwitch TIME_LIMIT = CommandSwitch + .argument("--time-limit", TimeArgument.time(-1)) + .getter(IntegerArgumentType::getInteger) + .build(() -> -1); private static final CommandSwitch AUTO_FORFEIT_TIME = CommandSwitch .argument("--auto-forfeit-time", TimeArgument.time(0)) .getter(IntegerArgumentType::getInteger) @@ -353,6 +357,23 @@ public Component getDisplayName() { ) ) ) + .then(literal("time-limit") + .requires(source -> source.hasPermission(2)) + .then(literal("set") + .then(argument("time-limit", TimeArgument.time()) + .executes(ctx -> { + final var game = ctx.getSource().getServer().bingo$getGame(); + if (game == null) { + throw NO_GAME_RUNNING.create(); + } + int time = IntegerArgumentType.getInteger(ctx, "time-limit"); + game.setScheduledEndTime(ctx.getSource().getServer().overworld().getGameTime() + time); + game.updateRemainingTime(ctx.getSource().getServer()); + return 1; + }) + ) + ) + ) ); { @@ -364,6 +385,7 @@ public Component getDisplayName() { SIZE.addTo(startCommand); SEED.addTo(startCommand); + TIME_LIMIT.addTo(startCommand); AUTO_FORFEIT_TIME.addTo(startCommand); DIFFICULTY.addTo(startCommand); GAMEMODE.addTo(startCommand); @@ -403,6 +425,7 @@ private static int startGame(CommandContext context, int tea final boolean continueAfterWin = CONTINUE_AFTER_WIN.get(context); final boolean includeInactiveTeams = INCLUDE_INACTIVE_TEAMS.get(context); final int autoForfeitTicks = AUTO_FORFEIT_TIME.get(context); + final int timeLimitTicks = TIME_LIMIT.get(context); final Set teams = LinkedHashSet.newLinkedHashSet(teamCount); for (int i = 1; i <= teamCount; i++) { @@ -452,6 +475,9 @@ private static int startGame(CommandContext context, int tea Bingo.LOGGER.info("Generated board (seed {}):\n{}", seed, board); final var game = new BingoGame(board, gamemode, requireClient, continueAfterWin, autoForfeitTicks, teams.toArray(PlayerTeam[]::new)); + if (timeLimitTicks > 0) { + game.setScheduledEndTime(context.getSource().getServer().overworld().getGameTime() + timeLimitTicks); + } server.bingo$setGame(game); Bingo.updateCommandTree(playerList); new ArrayList<>(playerList.getPlayers()).forEach(game::addPlayer); diff --git a/common/src/main/java/io/github/gaming32/bingo/client/BingoClient.java b/common/src/main/java/io/github/gaming32/bingo/client/BingoClient.java index db328bc4..f982f37e 100644 --- a/common/src/main/java/io/github/gaming32/bingo/client/BingoClient.java +++ b/common/src/main/java/io/github/gaming32/bingo/client/BingoClient.java @@ -16,6 +16,7 @@ import io.github.gaming32.bingo.platform.BingoPlatform; import io.github.gaming32.bingo.platform.event.ClientEvents; import io.github.gaming32.bingo.platform.registrar.KeyMappingBuilder; +import io.github.gaming32.bingo.util.BingoUtil; import io.github.gaming32.bingo.util.ResourceLocations; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; @@ -139,7 +140,35 @@ public static void renderBoardOnHud(Minecraft minecraft, GuiGraphics graphics) { final PositionAndScale pos = getBoardPosition(); renderBingo(graphics, minecraft.screen instanceof ChatScreen, pos); - if (CONFIG.isShowScoreCounter() && clientGame.renderMode() == BingoGameMode.RenderMode.ALL_TEAMS) { + final Font font = minecraft.font; + final int scoreX = (int)(pos.x() * pos.scale() + getBoardWidth() * pos.scale() / 2); + int scoreY; + if (CONFIG.getBoardCorner().isOnBottom) { + scoreY = (int)((pos.y() - BOARD_OFFSET) * pos.scale() - font.lineHeight); + } else { + scoreY = (int)(pos.y() * pos.scale() + (getBoardHeight() + BOARD_OFFSET) * pos.scale()); + } + final int shift = CONFIG.getBoardCorner().isOnBottom ? -12 : 12; + + if (clientGame.getScheduledEndTime() > 0) { + long serverTime = 0; + if (minecraft.level instanceof ClientLevel clientLevel) { + serverTime = clientLevel.getGameTime(); + } + long remainingTimeTicks = clientGame.getScheduledEndTime() - serverTime; + String formatedRemainingTime = " - " + BingoUtil.formatRemainingTime(remainingTimeTicks); + int color = 0xffffffff; + if (remainingTimeTicks < 30 * 60 * 20) + color = 0xffffaf00; + if (remainingTimeTicks < 5 * 60 * 20) + color = 0xffff0000; + final MutableComponent remainingTimeLabel = Component.translatable("bingo.remaining_time"); + graphics.drawString(font, remainingTimeLabel, scoreX - font.width(remainingTimeLabel), scoreY, color); + graphics.drawString(font, Component.literal(formatedRemainingTime), scoreX, scoreY, color); + scoreY += shift; + } + + if (CONFIG.isShowScoreCounter() && clientGame.getRenderMode() == BingoGameMode.RenderMode.ALL_TEAMS) { class TeamValue { final BingoBoard.Teams team; int score; @@ -149,13 +178,13 @@ class TeamValue { } } - final TeamValue[] teams = new TeamValue[clientGame.teams().length]; + final TeamValue[] teams = new TeamValue[clientGame.getTeams().length]; for (int i = 0; i < teams.length; i++) { teams[i] = new TeamValue(BingoBoard.Teams.fromOne(i)); } int totalScore = 0; - for (final BingoBoard.Teams state : clientGame.states()) { + for (final BingoBoard.Teams state : clientGame.getStates()) { if (state.any()) { totalScore++; teams[state.getFirstIndex()].score++; @@ -164,18 +193,9 @@ class TeamValue { Arrays.sort(teams, Comparator.comparing(v -> -v.score)); // Sort in reverse - final Font font = minecraft.font; - final int scoreX = (int)(pos.x() * pos.scale() + getBoardWidth() * pos.scale() / 2); - int scoreY; - if (CONFIG.getBoardCorner().isOnBottom) { - scoreY = (int)((pos.y() - BOARD_OFFSET) * pos.scale() - font.lineHeight); - } else { - scoreY = (int)(pos.y() * pos.scale() + (getBoardHeight() + BOARD_OFFSET) * pos.scale()); - } - final int shift = CONFIG.getBoardCorner().isOnBottom ? -12 : 12; for (final TeamValue teamValue : teams) { if (teamValue.score == 0) break; - final PlayerTeam team = clientGame.teams()[teamValue.team.getFirstIndex()]; + final PlayerTeam team = clientGame.getTeams()[teamValue.team.getFirstIndex()]; final MutableComponent leftText = getDisplayName(team).copy(); final MutableComponent rightText = Component.literal(" - " + teamValue.score); if (team.getColor() != ChatFormatting.RESET) { @@ -188,18 +208,18 @@ class TeamValue { } final MutableComponent leftText = Component.translatable("bingo.unclaimed"); - final MutableComponent rightText = Component.literal(" - " + (clientGame.states().length - totalScore)); + final MutableComponent rightText = Component.literal(" - " + (clientGame.getStates().length - totalScore)); graphics.drawString(font, leftText, scoreX - font.width(leftText), scoreY, 0xffffffff); graphics.drawString(font, rightText, scoreX, scoreY, 0xffffffff); } } public static int getBoardWidth() { - return 14 + 18 * clientGame.size(); + return 14 + 18 * clientGame.getSize(); } public static int getBoardHeight() { - return 24 + 18 * clientGame.size(); + return 24 + 18 * clientGame.getSize(); } public static void renderBingo(GuiGraphics graphics, boolean mouseHover, PositionAndScale pos) { @@ -213,13 +233,13 @@ public static void renderBingo(GuiGraphics graphics, boolean mouseHover, Positio graphics.pose().scale(pos.scale(), pos.scale(), 1); graphics.pose().translate(pos.x(), pos.y(), 0); - final BingoMousePos mousePos = mouseHover ? BingoMousePos.getPos(minecraft, clientGame.size(), pos) : null; + final BingoMousePos mousePos = mouseHover ? BingoMousePos.getPos(minecraft, clientGame.getSize(), pos) : null; graphics.blitSprite( RenderType::guiTextured, BOARD_TEXTURE, 0, 0, - 7 + 18 * clientGame.size() + 7, - 17 + 18 * clientGame.size() + 7 + 7 + 18 * clientGame.getSize() + 7, + 17 + 18 * clientGame.getSize() + 7 ); renderBoardTitle(graphics, minecraft.font); @@ -232,8 +252,8 @@ public static void renderBingo(GuiGraphics graphics, boolean mouseHover, Positio } final boolean spectator = minecraft.player != null && minecraft.player.isSpectator(); - for (int sx = 0; sx < clientGame.size(); sx++) { - for (int sy = 0; sy < clientGame.size(); sy++) { + for (int sx = 0; sx < clientGame.getSize(); sx++) { + for (int sy = 0; sy < clientGame.getSize(); sy++) { final var goal = clientGame.getGoal(sx, sy); final int slotX = sx * 18 + 8; final int slotY = sy * 18 + 18; @@ -244,14 +264,14 @@ public static void renderBingo(GuiGraphics graphics, boolean mouseHover, Positio final BingoBoard.Teams state = clientGame.getState(sx, sy); boolean isGoalCompleted = state.and(clientTeam); - final Integer color = switch (clientGame.renderMode()) { + final Integer color = switch (clientGame.getRenderMode()) { case FANCY -> isGoalCompleted ? Integer.valueOf(0x55ff55) : goal.specialType().incompleteColor; case ALL_TEAMS -> { if (!state.any()) { yield null; } final BingoBoard.Teams team = isGoalCompleted ? clientTeam : state; - final Integer maybeColor = clientGame.teams()[team.getFirstIndex()].getColor().getColor(); + final Integer maybeColor = clientGame.getTeams()[team.getFirstIndex()].getColor().getColor(); yield maybeColor != null ? maybeColor : 0x55ff55; } }; @@ -338,7 +358,7 @@ public static boolean detectClickOrPress(InputConstants.Key key, PositionAndScal return false; } - final BingoMousePos mousePos = BingoMousePos.getPos(Minecraft.getInstance(), clientGame.size(), boardPos); + final BingoMousePos mousePos = BingoMousePos.getPos(Minecraft.getInstance(), clientGame.getSize(), boardPos); if (!mousePos.hasSlotPos()) { return false; } diff --git a/common/src/main/java/io/github/gaming32/bingo/client/BoardScreen.java b/common/src/main/java/io/github/gaming32/bingo/client/BoardScreen.java index 89d3d53d..108c04d9 100644 --- a/common/src/main/java/io/github/gaming32/bingo/client/BoardScreen.java +++ b/common/src/main/java/io/github/gaming32/bingo/client/BoardScreen.java @@ -41,7 +41,7 @@ public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTi BingoClient.renderBingo(graphics, true, pos); assert minecraft != null; if (minecraft.player != null && minecraft.player.isSpectator()) { - final PlayerTeam team = BingoClient.clientGame.teams()[BingoClient.clientTeam.getFirstIndex()]; + final PlayerTeam team = BingoClient.clientGame.getTeams()[BingoClient.clientTeam.getFirstIndex()]; final Integer color = team.getColor().getColor(); graphics.drawCenteredString( font, @@ -105,7 +105,7 @@ private void updateButtonVisibility() { private void switchTeam(int dir) { final int currentIndex = BingoClient.clientTeam.getFirstIndex(); - final int newIndex = Math.floorMod(currentIndex + dir, BingoClient.clientGame.teams().length); + final int newIndex = Math.floorMod(currentIndex + dir, BingoClient.clientGame.getTeams().length); BingoClient.clientTeam = BingoBoard.Teams.fromOne(newIndex); } } diff --git a/common/src/main/java/io/github/gaming32/bingo/client/ClientGame.java b/common/src/main/java/io/github/gaming32/bingo/client/ClientGame.java index d65a47ce..01577cd5 100644 --- a/common/src/main/java/io/github/gaming32/bingo/client/ClientGame.java +++ b/common/src/main/java/io/github/gaming32/bingo/client/ClientGame.java @@ -7,14 +7,31 @@ import net.minecraft.world.scores.PlayerTeam; import org.jetbrains.annotations.Nullable; -public record ClientGame( - int size, - BingoBoard.Teams[] states, - ActiveGoal[] goals, - PlayerTeam[] teams, - BingoGameMode.RenderMode renderMode, - @Nullable GoalProgress[] progress -) { +public final class ClientGame { + private final int size; + private final BingoBoard.Teams[] states; + private final ActiveGoal[] goals; + private final PlayerTeam[] teams; + private final BingoGameMode.RenderMode renderMode; + private final GoalProgress @Nullable [] progress; + private long scheduledEndTime = -1; + + public ClientGame( + int size, + BingoBoard.Teams[] states, + ActiveGoal[] goals, + PlayerTeam[] teams, + BingoGameMode.RenderMode renderMode, + @Nullable GoalProgress[] progress + ) { + this.size = size; + this.states = states; + this.goals = goals; + this.teams = teams; + this.renderMode = renderMode; + this.progress = progress; + } + public BingoBoard.Teams getState(int x, int y) { return states[getIndex(x, y)]; } @@ -23,11 +40,43 @@ public ActiveGoal getGoal(int x, int y) { return goals[getIndex(x, y)]; } + public GoalProgress[] getProgress() { + return progress; + } + @Nullable public GoalProgress getProgress(int x, int y) { return progress[getIndex(x, y)]; } + public int getSize() { + return size; + } + + public BingoBoard.Teams[] getStates() { + return states; + } + + public ActiveGoal[] getGoals() { + return goals; + } + + public PlayerTeam[] getTeams() { + return teams; + } + + public BingoGameMode.RenderMode getRenderMode() { + return renderMode; + } + + public void setScheduledEndTime(long endTime) { + this.scheduledEndTime = endTime; + } + + public long getScheduledEndTime() { + return scheduledEndTime; + } + private int getIndex(int x, int y) { return y * size + x; } diff --git a/common/src/main/java/io/github/gaming32/bingo/client/ClientPayloadHandlerImpl.java b/common/src/main/java/io/github/gaming32/bingo/client/ClientPayloadHandlerImpl.java index 53a444f1..c78aabc2 100644 --- a/common/src/main/java/io/github/gaming32/bingo/client/ClientPayloadHandlerImpl.java +++ b/common/src/main/java/io/github/gaming32/bingo/client/ClientPayloadHandlerImpl.java @@ -6,6 +6,7 @@ import io.github.gaming32.bingo.network.messages.s2c.InitBoardPayload; import io.github.gaming32.bingo.network.messages.s2c.ResyncStatesPayload; import io.github.gaming32.bingo.network.messages.s2c.SyncTeamPayload; +import io.github.gaming32.bingo.network.messages.s2c.UpdateEndTimePayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateProgressPayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateStatePayload; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; @@ -50,8 +51,8 @@ public void handleResyncStates(ResyncStatesPayload payload) { if (!checkGamePresent(payload)) return; System.arraycopy( payload.states(), 0, - BingoClient.clientGame.states(), 0, - BingoClient.clientGame.size() * BingoClient.clientGame.size() + BingoClient.clientGame.getStates(), 0, + BingoClient.clientGame.getSize() * BingoClient.clientGame.getSize() ); } @@ -63,18 +64,24 @@ public void handleSyncTeam(SyncTeamPayload payload) { @Override public void handleUpdateProgress(UpdateProgressPayload payload) { if (!checkGamePresent(payload)) return; - BingoClient.clientGame.progress()[payload.index()] = new GoalProgress(payload.progress(), payload.maxProgress()); + BingoClient.clientGame.getProgress()[payload.index()] = new GoalProgress(payload.progress(), payload.maxProgress()); } @Override public void handleUpdateState(UpdateStatePayload payload) { if (!checkGamePresent(payload)) return; final int index = payload.index(); - if (index < 0 || index >= BingoClient.clientGame.size() * BingoClient.clientGame.size()) { + if (index < 0 || index >= BingoClient.clientGame.getSize() * BingoClient.clientGame.getSize()) { Bingo.LOGGER.warn("Invalid {} payload: invalid board index {}", UpdateStatePayload.TYPE, index); return; } - BingoClient.clientGame.states()[index] = payload.newState(); + BingoClient.clientGame.getStates()[index] = payload.newState(); + } + + @Override + public void handleUpdateEndTime(UpdateEndTimePayload payload) { + if (!checkGamePresent(payload)) return; + BingoClient.clientGame.setScheduledEndTime(payload.endTime()); } private static boolean checkGamePresent(CustomPacketPayload payload) { diff --git a/common/src/main/java/io/github/gaming32/bingo/client/TeamSelectionSpectatorCategory.java b/common/src/main/java/io/github/gaming32/bingo/client/TeamSelectionSpectatorCategory.java index 71ca1d42..b9943b7b 100644 --- a/common/src/main/java/io/github/gaming32/bingo/client/TeamSelectionSpectatorCategory.java +++ b/common/src/main/java/io/github/gaming32/bingo/client/TeamSelectionSpectatorCategory.java @@ -45,7 +45,7 @@ private static List createTeamEntries(@Nullable ClientGame ga return List.of(); } final MutableInt teamId = new MutableInt(); - return Arrays.stream(game.teams()) + return Arrays.stream(game.getTeams()) .map(team -> TeamSelectionItem.create(game, team, BingoBoard.Teams.fromOne(teamId.getAndIncrement()))) .filter(Objects::nonNull) .toList(); @@ -107,9 +107,9 @@ public static SpectatorMenuItem create(ClientGame game, PlayerTeam team, BingoBo if (onlinePlayers.isEmpty()) { boolean hasAnyGoals = false; - for (int i = 0; i < game.states().length; i++) { - boolean hasGoal = game.states()[i].and(teamId); - if (game.goals()[i].specialType() == BingoTag.SpecialType.NEVER) { + for (int i = 0; i < game.getStates().length; i++) { + boolean hasGoal = game.getStates()[i].and(teamId); + if (game.getGoals()[i].specialType() == BingoTag.SpecialType.NEVER) { hasGoal = !hasGoal; } if (hasGoal) { diff --git a/common/src/main/java/io/github/gaming32/bingo/game/BingoGame.java b/common/src/main/java/io/github/gaming32/bingo/game/BingoGame.java index c3c27bc8..c970204a 100644 --- a/common/src/main/java/io/github/gaming32/bingo/game/BingoGame.java +++ b/common/src/main/java/io/github/gaming32/bingo/game/BingoGame.java @@ -13,6 +13,7 @@ import io.github.gaming32.bingo.network.messages.s2c.RemoveBoardPayload; import io.github.gaming32.bingo.network.messages.s2c.ResyncStatesPayload; import io.github.gaming32.bingo.network.messages.s2c.SyncTeamPayload; +import io.github.gaming32.bingo.network.messages.s2c.UpdateEndTimePayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateProgressPayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateStatePayload; import io.github.gaming32.bingo.triggers.progress.ProgressibleTrigger; @@ -41,12 +42,14 @@ import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.protocol.game.ClientboundUpdateAdvancementsPacket; import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerBossEvent; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.PlayerList; import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.stats.Stat; import net.minecraft.util.ExtraCodecs; +import net.minecraft.world.BossEvent; import net.minecraft.world.entity.player.Player; import net.minecraft.world.scores.PlayerTeam; import net.minecraft.world.scores.Scoreboard; @@ -83,10 +86,12 @@ public class BingoGame { private final Map> goalAchievedCount = new HashMap<>(); private final Map> queuedGoals = new HashMap<>(); private final Map>> baseStats = new HashMap<>(); + private final ServerBossEvent vanillaRemainingTime = new ServerBossEvent(Bingo.translatable("bingo.remaining_time"), BossEvent.BossBarColor.WHITE, BossEvent.BossBarOverlay.PROGRESS); private final OptionalLong[] lastActiveTimes; private BingoBoard.Teams remainingTeams; private BingoBoard.Teams winningTeams = BingoBoard.Teams.NONE; private BingoBoard.Teams finishedTeams = BingoBoard.Teams.NONE; + private long scheduledEndTime = -1; public BingoGame(BingoBoard board, BingoGameMode gameMode, boolean requireClient, boolean continueAfterWin, int autoForfeitTicks, PlayerTeam... teams) { this.board = board; @@ -116,6 +121,14 @@ public boolean shouldContinueAfterWin() { return continueAfterWin; } + public void setScheduledEndTime(long endTime) { + this.scheduledEndTime = endTime; + } + + public long getScheduledEndTime() { + return scheduledEndTime; + } + /** * @apiNote {@code player} does not need to be in a team. In fact, they should be added even if they aren't! */ @@ -151,6 +164,14 @@ public void addPlayer(ServerPlayer player) { } }); } + + if (scheduledEndTime > 0) { + if (Bingo.isInstalledOnClient(player)) { + new UpdateEndTimePayload(scheduledEndTime).sendTo(player); + } else { + vanillaRemainingTime.addPlayer(player); + } + } } public void syncAdvancementsTo(ServerPlayer player) { @@ -166,6 +187,36 @@ public void syncAdvancementsTo(ServerPlayer player) { public void removePlayer(ServerPlayer player) { unregisterListeners(player, true); + vanillaRemainingTime.removePlayer(player); + } + + public void updateRemainingTime(MinecraftServer server) { + for (ServerPlayer player : server.getPlayerList().getPlayers()) { + if (Bingo.isInstalledOnClient(player)) { + new UpdateEndTimePayload(scheduledEndTime).sendTo(player); + } + } + updateVanillaRemainingTime(server); + } + + public void updateVanillaRemainingTime(MinecraftServer server) { + if (vanillaRemainingTime.getPlayers().isEmpty()) { + return; + } + long remainingTimeTicks = getScheduledEndTime() - server.overworld().getGameTime(); + String formatedRemainingTime = BingoUtil.formatRemainingTime(remainingTimeTicks); + if (remainingTimeTicks <= 0) { + vanillaRemainingTime.removeAllPlayers(); + return; + } + BossEvent.BossBarColor color = BossEvent.BossBarColor.WHITE; + if (remainingTimeTicks < 5 * 60 * 20) { + color = BossEvent.BossBarColor.RED; + } else if (remainingTimeTicks < 30 * 60 * 20) { + color = BossEvent.BossBarColor.PURPLE; + } + vanillaRemainingTime.setName(Bingo.translatable("bingo.remaining_time_with_value", formatedRemainingTime)); + vanillaRemainingTime.setColor(color); } public BingoBoard.Teams[] obfuscateTeam(BingoBoard.Teams playerTeam, Player player) { @@ -237,6 +288,10 @@ public void endGame(PlayerList playerList) { playerList.getServer().bingo$setGame(null); new ResyncStatesPayload(board.getStates()).sendTo(playerList.getPlayers()); + if (getScheduledEndTime() > 0) { + setScheduledEndTime(-1); + updateRemainingTime(playerList.getServer()); + } Bingo.updateCommandTree(playerList); } @@ -266,6 +321,14 @@ public void tick(MinecraftServer server) { } } } + + if (getScheduledEndTime() > 0 && server.getTickCount() % 20 == 0) { + if (server.overworld().getGameTime() > getScheduledEndTime()) { + endGame(server.getPlayerList()); + } else { + updateVanillaRemainingTime(server); + } + } } public boolean forfeit(PlayerList playerList, BingoBoard.Teams team) { diff --git a/common/src/main/java/io/github/gaming32/bingo/network/ClientPayloadHandler.java b/common/src/main/java/io/github/gaming32/bingo/network/ClientPayloadHandler.java index 6dcd3e15..2024ccdf 100644 --- a/common/src/main/java/io/github/gaming32/bingo/network/ClientPayloadHandler.java +++ b/common/src/main/java/io/github/gaming32/bingo/network/ClientPayloadHandler.java @@ -3,6 +3,7 @@ import io.github.gaming32.bingo.network.messages.s2c.InitBoardPayload; import io.github.gaming32.bingo.network.messages.s2c.ResyncStatesPayload; import io.github.gaming32.bingo.network.messages.s2c.SyncTeamPayload; +import io.github.gaming32.bingo.network.messages.s2c.UpdateEndTimePayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateProgressPayload; import io.github.gaming32.bingo.network.messages.s2c.UpdateStatePayload; import io.github.gaming32.bingo.platform.BingoPlatform; @@ -21,6 +22,8 @@ public interface ClientPayloadHandler { void handleUpdateState(UpdateStatePayload payload); + void handleUpdateEndTime(UpdateEndTimePayload payload); + static ClientPayloadHandler get() { if (Holder.instance == null) { if (BingoPlatform.platform.isClient()) { diff --git a/common/src/main/java/io/github/gaming32/bingo/network/messages/s2c/UpdateEndTimePayload.java b/common/src/main/java/io/github/gaming32/bingo/network/messages/s2c/UpdateEndTimePayload.java new file mode 100644 index 00000000..63fff326 --- /dev/null +++ b/common/src/main/java/io/github/gaming32/bingo/network/messages/s2c/UpdateEndTimePayload.java @@ -0,0 +1,25 @@ +package io.github.gaming32.bingo.network.messages.s2c; + +import io.github.gaming32.bingo.network.AbstractCustomPayload; +import io.github.gaming32.bingo.network.BingoNetworking; +import io.github.gaming32.bingo.network.ClientPayloadHandler; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.NotNull; + +public record UpdateEndTimePayload(long endTime) implements AbstractCustomPayload { + public static final Type TYPE = AbstractCustomPayload.type("update_end_time"); + public static final StreamCodec CODEC = ByteBufCodecs.VAR_LONG.map(UpdateEndTimePayload::new, UpdateEndTimePayload::endTime); + + @NotNull + @Override + public Type type() { + return TYPE; + } + + @Override + public void handle(BingoNetworking.Context context) { + ClientPayloadHandler.get().handleUpdateEndTime(this); + } +} diff --git a/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java b/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java index ca146b72..51a2443e 100644 --- a/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java +++ b/common/src/main/java/io/github/gaming32/bingo/util/BingoUtil.java @@ -216,6 +216,21 @@ public static Either getDisplayName(PlayerTeam team, Playe return Either.right(team.getDisplayName()); } + public static String formatRemainingTime(long remainingTimeTicks) { + if (remainingTimeTicks < 0) + remainingTimeTicks = 0; + int hours = (int) (remainingTimeTicks / 20 / 60 / 60); + int minutes = (int) (remainingTimeTicks / 20 / 60 % 60); + int seconds = (int) (remainingTimeTicks / 20 % 60); + StringBuilder sb = new StringBuilder(); + if (hours > 0) { + sb.append(hours); + sb.append(":"); + } + sb.append(String.format("%02d:%02d", minutes, seconds)); + return sb.toString(); + } + public static Either mapEither(Either either, Function mapper) { return either.mapBoth(mapper, mapper); } diff --git a/common/src/main/resources/assets/bingo/lang/en_us.json b/common/src/main/resources/assets/bingo/lang/en_us.json index b495292f..add8e6f0 100644 --- a/common/src/main/resources/assets/bingo/lang/en_us.json +++ b/common/src/main/resources/assets/bingo/lang/en_us.json @@ -308,6 +308,8 @@ "bingo.key.category": "Bingo", "bingo.sixteen_bang": "%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, and %s!", "bingo.unclaimed": "Unclaimed", + "bingo.remaining_time": "Remaining time", + "bingo.remaining_time_with_value": "Remaining time: %s", "bingo.three": "%s, %s, and %s", "bingo.four": "%s, %s, %s, and %s", "bingo.spawner": "%s %s",