diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java index 7b7954c..331c1c6 100644 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoro.java @@ -6,70 +6,77 @@ package eu.m724.tweaks.module.pomodoro; +import java.util.concurrent.TimeUnit; + public class PlayerPomodoro { - private int pomodori = 0; + private int intervalsDone = 0; - private boolean isBreak = false; - // this is for both break and not break - private long intervalStart = -1; + private boolean breaktime = false; + private long currentIntervalStartedAtNanos = -1; - /** - * A "pomodoro" is the 25-minute cycle you take breaks after
- * This returns how many cycles already elapsed, so if this is the first cycle this is 0
- * The break after the "pomodoro," so if it's breaktime after the first "pomodoro" it stays at 0 - */ - public int getPomodori() { - return pomodori; + public int getIntervalsDone() { + return intervalsDone; + } + + public boolean isBreaktime() { + return breaktime; + } + + public boolean isLongBreak() { + return (intervalsDone + 1) % PomodoroModule.INTERVALS_BEFORE_LONG_BREAK == 0; } /** - * When did the current interval start
- * Or when did the break start - * - * @see PlayerPomodoro#isBreak() + * @return The time the current interval or break started, in milliseconds */ - public long getIntervalStart() { - return intervalStart; + public long getCurrentIntervalStartedAtNanos() { + return currentIntervalStartedAtNanos; } - public int getCycleDurationSeconds() { - return isBreak ? (pomodori < 3 ? 300 : 1200) : 1500; + public int getCurrentIntervalDurationSeconds() { + if (breaktime) { + if (isLongBreak()) { + return PomodoroModule.LONG_BREAK_DURATION_SECONDS; + } + + return PomodoroModule.SHORT_BREAK_DURATION_SECONDS; + } else { + return PomodoroModule.INTERVAL_DURATION_SECONDS; + } } - public long getRemainingSeconds(long now) { - return getCycleDurationSeconds() - (now - getIntervalStart()) / 1000000000; + public long getCurrentIntervalDurationNanos() { + return TimeUnit.SECONDS.toNanos(getCurrentIntervalDurationSeconds()); } - /** - * Is it a break currently - */ - public boolean isBreak() { - return isBreak; + public long getCurrentIntervalRemainingNanos(long nowNanos) { + long elapsed = nowNanos - getCurrentIntervalStartedAtNanos(); + return getCurrentIntervalDurationNanos() - elapsed; } - public boolean isCycleComplete() { - return intervalStart + getCycleDurationSeconds() * 1000000000L < System.nanoTime(); + public boolean isIntervalComplete(long nowNanos) { + return currentIntervalStartedAtNanos + getCurrentIntervalDurationNanos() < nowNanos; } /** * Resets and starts the timer */ public void start() { - this.pomodori = 0; - this.isBreak = false; - this.intervalStart = System.nanoTime(); + this.intervalsDone = 0; + this.breaktime = false; + this.currentIntervalStartedAtNanos = System.nanoTime(); } /** * Completes a cycle */ - public void next() { - if (isBreak) { // from break to interval - this.pomodori++; - this.pomodori %= 4; + public void startBreakOrNextInterval() { + if (breaktime) { // from break to interval + this.intervalsDone++; + this.intervalsDone %= PomodoroModule.INTERVALS_BEFORE_LONG_BREAK; } - this.intervalStart = System.nanoTime(); - isBreak = !isBreak; + this.currentIntervalStartedAtNanos = System.nanoTime(); + breaktime = !breaktime; } } diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java new file mode 100644 index 0000000..92e3c6f --- /dev/null +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PlayerPomodoroTracker.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Minecon724 + * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file + * in the project root for the full license text. + */ + +package eu.m724.tweaks.module.pomodoro; + +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class PlayerPomodoroTracker { + static final Map timers = new HashMap<>(); + + public static PlayerPomodoro get(Player player) { + return timers.get(player.getUniqueId()); + } + + public static PlayerPomodoro create(Player player) { + return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro()); + } + + public static boolean remove(Player player) { + return timers.remove(player.getUniqueId()) != null; + } +} diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java index 55f9685..ee452ab 100644 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroCommands.java @@ -6,6 +6,9 @@ package eu.m724.tweaks.module.pomodoro; +import eu.m724.tweaks.Language; +import eu.m724.tweaks.config.TweaksConfig; +import net.md_5.bungee.api.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -18,25 +21,34 @@ public class PomodoroCommands implements CommandExecutor { Player player = (Player) sender; String action = args.length > 0 ? args[0] : null; - PlayerPomodoro pomodoro = Pomodoros.get(player); + PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player); + + long now = System.nanoTime(); if (pomodoro != null) { if ("stop".equals(action)) { - Pomodoros.remove(player); - sender.sendMessage("Pomodoro disabled"); + PlayerPomodoroTracker.remove(player); + sender.spigot().sendMessage(Language.getComponent("pomodoroStopped", ChatColor.GREEN, label)); } else { - if (pomodoro.isCycleComplete()) { - pomodoro.next(); + if (pomodoro.isIntervalComplete(now)) { + pomodoro.startBreakOrNextInterval(); + + if (pomodoro.isBreaktime() && TweaksConfig.getConfig().pomodoroForce()) { + player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText()); + } } - sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(System.nanoTime()))); + + sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now)); } } else { if ("start".equals(action)) { - pomodoro = Pomodoros.create(player); + pomodoro = PlayerPomodoroTracker.create(player); pomodoro.start(); - sender.spigot().sendMessage(Pomodoros.formatTimer(pomodoro, pomodoro.getCycleDurationSeconds())); + + sender.spigot().sendMessage(PomodoroModule.formatTimer(pomodoro, now)); } else { - sender.sendMessage("Start pomodoro with /pom start"); + // TODO help? + sender.spigot().sendMessage(Language.getComponent("pomodoroStart", ChatColor.GOLD, label)); } } diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java index 077e36e..853215e 100644 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroListener.java @@ -7,56 +7,68 @@ package eu.m724.tweaks.module.pomodoro; import eu.m724.tweaks.config.TweaksConfig; -import net.md_5.bungee.api.chat.ComponentBuilder; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.*; +import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.event.player.PlayerToggleSneakEvent; public class PomodoroListener implements Listener { - private final boolean force = TweaksConfig.getConfig().pomodoroForce(); + private final TweaksConfig config = TweaksConfig.getConfig(); @EventHandler public void onPlayerLogin(PlayerLoginEvent event) { + // Joining ends break Player player = event.getPlayer(); - PlayerPomodoro pomodoro = Pomodoros.get(player); + PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player); if (pomodoro == null) return; - long remaining = pomodoro.getRemainingSeconds(System.nanoTime()); + long now = System.nanoTime(); + long remaining = pomodoro.getCurrentIntervalRemainingNanos(now); - if (pomodoro.isBreak()) { - if (pomodoro.isCycleComplete()) { - pomodoro.next(); + if (pomodoro.isBreaktime()) { + if (remaining > 0 && config.pomodoroForce()) { + player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText()); } else { - if (force) { - event.getPlayer().kickPlayer( - new ComponentBuilder() - .append(Pomodoros.formatTimer(pomodoro, remaining)) - .build().toLegacyText() - ); - } + pomodoro.startBreakOrNextInterval(); } } } @EventHandler public void onPlayerQuit(PlayerQuitEvent event) { + // Quitting starts break Player player = event.getPlayer(); - PlayerPomodoro pomodoro = Pomodoros.get(player); + PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player); if (pomodoro == null) return; - if (!pomodoro.isBreak() && pomodoro.isCycleComplete()) { - pomodoro.next(); + if (pomodoro.isBreaktime()) return; + + long now = System.nanoTime(); + long remaining = pomodoro.getCurrentIntervalRemainingNanos(now); + + if (remaining <= 0) { + pomodoro.startBreakOrNextInterval(); } } @EventHandler - public void onPlayerMove(PlayerMoveEvent event) { - Player player = event.getPlayer(); - PlayerPomodoro timer = Pomodoros.get(player); - if (timer == null) return; + public void onPlayerToggleSneak(PlayerToggleSneakEvent event) { + // Sneaking ends break + if (!event.isSneaking()) return; - if (timer.isBreak() && timer.getRemainingSeconds(System.nanoTime()) <= 0) - timer.next(); // resume timer if break ended + Player player = event.getPlayer(); + PlayerPomodoro pomodoro = PlayerPomodoroTracker.get(player); + if (pomodoro == null) return; + + if (!pomodoro.isBreaktime()) return; + + long now = System.nanoTime(); + long remaining = pomodoro.getCurrentIntervalRemainingNanos(now); + + if (remaining <= 0) { + pomodoro.startBreakOrNextInterval(); + } } } diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java index 89dd3d5..c97da7d 100644 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroModule.java @@ -6,9 +6,23 @@ package eu.m724.tweaks.module.pomodoro; +import eu.m724.tweaks.Language; +import eu.m724.tweaks.config.TweaksConfig; import eu.m724.tweaks.module.TweaksModule; +import net.md_5.bungee.api.ChatColor; +import net.md_5.bungee.api.chat.BaseComponent; +import net.md_5.bungee.api.chat.ComponentBuilder; + +import java.util.concurrent.TimeUnit; public class PomodoroModule extends TweaksModule { + static final int INTERVAL_DURATION_SECONDS = 10; + static final int SHORT_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(5); + static final int LONG_BREAK_DURATION_SECONDS = (int) TimeUnit.MINUTES.toSeconds(20); + static final int INTERVALS_BEFORE_LONG_BREAK = 4; + + static final int KICK_DELAY_SECONDS = 60; + @Override protected void onInit() { registerEvents(new PomodoroListener()); @@ -16,4 +30,66 @@ public class PomodoroModule extends TweaksModule { registerCommand("pomodoro", new PomodoroCommands()); } + + /** + * Gets a formatted timer for a player. + * + * @param pomodoro the player's {@link PlayerPomodoro} instance + * @param nowNanos unix now timestamp in nanoseconds + * @return the timer as {@link BaseComponent} + */ + static BaseComponent formatTimer(PlayerPomodoro pomodoro, long nowNanos) { + ComponentBuilder builder = new ComponentBuilder(); + + long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(nowNanos); + long remainingSeconds = TimeUnit.NANOSECONDS.toSeconds(remainingNanos); + + if (pomodoro.isBreaktime()) { + if (pomodoro.isLongBreak()) { + builder.append(Language.getComponent("pomodoroLongBreak", ChatColor.LIGHT_PURPLE)); + } else { + builder.append(Language.getComponent("pomodoroShortBreak", ChatColor.LIGHT_PURPLE)); + } + + if (remainingNanos > 0) { + builder.append(" %02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60)) + .color(ChatColor.GOLD); + } else { + builder.append(" 00:00") + .color(ChatColor.GREEN); + + if (!TweaksConfig.getConfig().pomodoroForce()) { + builder.append( "/pom").color(ChatColor.GOLD); + } + } + } else { + if (remainingNanos > 0) { + builder + .append("%02d:%02d".formatted(remainingSeconds / 60, remainingSeconds % 60)) + .color(ChatColor.GRAY); + } else { + builder + .append("00:00") + .color(remainingSeconds % 2 == 0 ? ChatColor.RED : ChatColor.GRAY); + + if (!TweaksConfig.getConfig().pomodoroForce()) { + builder.append( "/pom").color(ChatColor.GOLD); + } + } + } + + + for (int i=0; i pomodoro.getIntervalsDone()) { + color = ChatColor.DARK_GRAY; + } + + builder.append(" o").color(color); + } + + return builder.build(); + } } diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java index 1b70d5f..f9bd924 100644 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java +++ b/src/main/java/eu/m724/tweaks/module/pomodoro/PomodoroRunnable.java @@ -9,34 +9,49 @@ package eu.m724.tweaks.module.pomodoro; import eu.m724.tweaks.TweaksPlugin; import eu.m724.tweaks.config.TweaksConfig; import net.md_5.bungee.api.ChatMessageType; -import org.bukkit.Bukkit; import org.bukkit.Sound; +import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitRunnable; +import java.util.concurrent.TimeUnit; + public class PomodoroRunnable extends BukkitRunnable { - private final boolean force = TweaksConfig.getConfig().pomodoroForce(); + private final TweaksConfig config = TweaksConfig.getConfig(); private final Plugin plugin = TweaksPlugin.getInstance(); // used only to kick @Override public void run() { long now = System.nanoTime(); - Bukkit.getOnlinePlayers().forEach(player -> { - PlayerPomodoro pomodoro = Pomodoros.get(player); - if (pomodoro == null) return; + PlayerPomodoroTracker.timers.forEach((uuid, pomodoro) -> { + long remainingNanos = pomodoro.getCurrentIntervalRemainingNanos(now); + long remainingSecs = TimeUnit.NANOSECONDS.toSeconds(remainingNanos); - long remaining = pomodoro.getRemainingSeconds(now); - // TODO make not always on - player.spigot().sendMessage(ChatMessageType.ACTION_BAR, Pomodoros.formatTimer(pomodoro, remaining)); + // TODO optimize? + Player player = plugin.getServer().getPlayer(uuid); - if (remaining <= 0) { - player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f); - if (remaining < -60 && force) { - plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> { - pomodoro.next(); - player.kickPlayer(Pomodoros.formatTimer(pomodoro, pomodoro.getRemainingSeconds(now)).toLegacyText()); - }); + if (player != null && player.isOnline()) { + // TODO make not always on + player.spigot().sendMessage(ChatMessageType.ACTION_BAR, PomodoroModule.formatTimer(pomodoro, now)); + + if (remainingNanos <= 0) { + player.playSound(player.getLocation(), Sound.BLOCK_ANVIL_FALL, 1.0f, 0.5f); + + if (remainingSecs < -PomodoroModule.KICK_DELAY_SECONDS && config.pomodoroForce()) { + pomodoro.startBreakOrNextInterval(); + + plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, () -> + player.kickPlayer(PomodoroModule.formatTimer(pomodoro, now).toLegacyText()) + ); + } + } + } else { + if (remainingNanos <= 0) { + // Start break automatically if the player is offline + if (!pomodoro.isBreaktime()) { + pomodoro.startBreakOrNextInterval(); + } } } }); diff --git a/src/main/java/eu/m724/tweaks/module/pomodoro/Pomodoros.java b/src/main/java/eu/m724/tweaks/module/pomodoro/Pomodoros.java deleted file mode 100644 index 43cb412..0000000 --- a/src/main/java/eu/m724/tweaks/module/pomodoro/Pomodoros.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2025 Minecon724 - * Tweaks724 is licensed under the GNU General Public License. See the LICENSE.md file - * in the project root for the full license text. - */ - -package eu.m724.tweaks.module.pomodoro; - -import net.md_5.bungee.api.ChatColor; -import net.md_5.bungee.api.chat.BaseComponent; -import net.md_5.bungee.api.chat.ComponentBuilder; -import org.bukkit.entity.Player; - -import java.util.HashMap; -import java.util.Map; -import java.util.UUID; - -public class Pomodoros { - static final Map timers = new HashMap<>(); - - public static PlayerPomodoro get(Player player) { - return timers.get(player.getUniqueId()); - } - - public static PlayerPomodoro create(Player player) { - return timers.computeIfAbsent(player.getUniqueId(), (k) -> new PlayerPomodoro()); - } - - public static boolean remove(Player player) { - return timers.remove(player.getUniqueId()) != null; - } - - static BaseComponent formatTimer(PlayerPomodoro pomodoro, long remaining) { - ComponentBuilder builder = new ComponentBuilder(); - - if (pomodoro.isBreak()) { - builder.append("Break ").color(ChatColor.LIGHT_PURPLE); - if (remaining > 0) { - builder.append("%02d:%02d".formatted(remaining / 60, remaining % 60)) - .color(ChatColor.GOLD); - } else { - builder.append("00:00") - .color(ChatColor.GREEN); - } - } else { - if (remaining > 0) { - builder - .append("%02d:%02d".formatted(remaining / 60, remaining % 60)) - .color(ChatColor.GRAY); - } else { - builder - .append("00:00") - .color(remaining % 2 == 0 ? ChatColor.RED : ChatColor.YELLOW); - } - } - - - for (int i=0; i<4; i++) { - ChatColor color = ChatColor.GRAY; - if (i == pomodoro.getPomodori()) { - color = ChatColor.LIGHT_PURPLE; - } else if (i > pomodoro.getPomodori()) { - color = ChatColor.DARK_GRAY; - } - - builder.append(" o").color(color); - } - - return builder.build(); - } -} diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index b628eab..204bc20 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -30,7 +30,7 @@ chatAlreadyHere = You're already in this room. authKickWrongKey = You're connecting to the wrong server address. You must connect to the one you're registered to. # If force is enabled and player is not registered. Changing this reveals you're using this plugin authKickUnregistered = You are not whitelisted on this server! -authKickError = An error occured. Please try again. If this persists, contact an administrator. +authKickError = An error occurred. Please try again. If this persists, contact an administrator. redstoneGatewayItem = Redstone gateway @@ -44,4 +44,10 @@ durabilityDisabled = Disabled durability alert wordCoordsPlayerOnly = Only players can execute this command without arguments. wordCoordsOutOfRange = Those coordinates are invalid. wordCoordsInvalidWord = Invalid word: "%s" -wordCoordsNoWords = Please provide the Z coordinate. \ No newline at end of file +wordCoordsNoWords = Please provide the Z coordinate. + +# /pomodoro +pomodoroStopped = Pomodoro stopped. Restart it with /%s start +pomodoroStart = Start pomodoro with /%s start +pomodoroShortBreak = Short break +pomodoroLongBreak = Long break \ No newline at end of file