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