diff --git a/pom.xml b/pom.xml
index de74587..7846d54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,17 +17,13 @@
spigot-repo
https://hub.spigotmc.org/nexus/content/repositories/snapshots/
-
- sonatype
- https://oss.sonatype.org/content/groups/public/
-
org.spigotmc
spigot-api
- 1.21.5-R0.1-SNAPSHOT
+ 1.21-R0.1-SNAPSHOT
provided
@@ -48,14 +44,14 @@
org.apache.commons
commons-lang3
- 3.17.0
+ 3.18.0
provided
-
- com.github.stefvanschie.inventoryframework
- IF
- 0.11.0
+ org.jetbrains
+ annotations
+ 26.0.2
+ compile
@@ -87,21 +83,7 @@
3.6.0
false
-
-
- com.github.stefvanschie.inventoryframework
- com.projectkorra.rpg.inventoryframework
-
-
-
-
- package
-
- shade
-
-
-
diff --git a/src/main/java/com/projectkorra/rpg/ProjectKorraRPG.java b/src/main/java/com/projectkorra/rpg/ProjectKorraRPG.java
index 684654b..1ee92b0 100644
--- a/src/main/java/com/projectkorra/rpg/ProjectKorraRPG.java
+++ b/src/main/java/com/projectkorra/rpg/ProjectKorraRPG.java
@@ -6,6 +6,8 @@
import com.projectkorra.rpg.metrics.MetricsLite;
import com.projectkorra.rpg.modules.ModuleManager;
import com.projectkorra.rpg.storage.TableCreator;
+import com.projectkorra.rpg.ui.service.InventoryEventListener;
+import com.projectkorra.rpg.ui.service.InventoryService;
import net.luckperms.api.LuckPerms;
import org.bukkit.Bukkit;
import org.bukkit.plugin.RegisteredServiceProvider;
@@ -18,6 +20,7 @@ public class ProjectKorraRPG extends JavaPlugin {
public static LuckPerms luckPermsAPI;
private ModuleManager moduleManager;
+ private InventoryService inventoryService;
@Override
public void onEnable() {
@@ -26,9 +29,11 @@ public void onEnable() {
new ConfigManager();
new TableCreator();
- moduleManager = new ModuleManager();
+ moduleManager = new ModuleManager(this);
+ inventoryService = new InventoryService();
- Bukkit.getServer().getPluginManager().registerEvents(new RPGListener(), plugin);
+ Bukkit.getServer().getPluginManager().registerEvents(new InventoryEventListener(inventoryService), this);
+ Bukkit.getServer().getPluginManager().registerEvents(new RPGListener(this), this);
RegisteredServiceProvider provider = Bukkit.getServicesManager().getRegistration(LuckPerms.class);
if (provider != null) {
@@ -44,7 +49,7 @@ public void onEnable() {
// Metrics
try {
- MetricsLite metrics = new MetricsLite(plugin);
+ MetricsLite metrics = new MetricsLite(this);
metrics.start();
} catch (IOException e) {
getLogger().severe("Failed to submit stats to bStats!" + e.getMessage());
@@ -54,6 +59,7 @@ public void onEnable() {
@Override
public void onDisable() {
moduleManager.disableModules();
+ inventoryService.closeAll();
}
public static ProjectKorraRPG getPlugin() {
@@ -64,6 +70,10 @@ public static LuckPerms getLuckPermsAPI() {
return luckPermsAPI;
}
+ public InventoryService getInventoryService() {
+ return inventoryService;
+ }
+
public ModuleManager getModuleManager() {
return moduleManager;
}
diff --git a/src/main/java/com/projectkorra/rpg/RPGListener.java b/src/main/java/com/projectkorra/rpg/RPGListener.java
index 90c0918..384aa26 100644
--- a/src/main/java/com/projectkorra/rpg/RPGListener.java
+++ b/src/main/java/com/projectkorra/rpg/RPGListener.java
@@ -1,6 +1,7 @@
package com.projectkorra.rpg;
import com.projectkorra.projectkorra.event.BendingReloadEvent;
+import com.projectkorra.projectkorra.util.ChatUtil;
import com.projectkorra.rpg.commands.HelpCommand;
import com.projectkorra.rpg.commands.RPGCommandBase;
import com.projectkorra.rpg.configuration.ConfigManager;
@@ -9,10 +10,16 @@
import org.bukkit.scheduler.BukkitRunnable;
public class RPGListener implements Listener {
+ private final ProjectKorraRPG plugin;
+
+ public RPGListener(final ProjectKorraRPG plugin) {
+ this.plugin = plugin;
+ }
+
@EventHandler
- public void onBendingConfigReload(BendingReloadEvent event) {
+ public void onBendingConfigReload(final BendingReloadEvent event) {
// Disable all enabled modules for clean module start
- ProjectKorraRPG.getPlugin().getModuleManager().disableModules();
+ plugin.getModuleManager().disableModules();
// Reload configs
ConfigManager.defaultConfig.reload();
@@ -24,10 +31,12 @@ public void onBendingConfigReload(BendingReloadEvent event) {
public void run() {
new RPGCommandBase();
new HelpCommand();
- }
- }.runTaskLater(ProjectKorraRPG.getPlugin(), 20);
+ }
+ }.runTaskLater(plugin, 20);
// Re-Enable all modules for clean start
- ProjectKorraRPG.getPlugin().getModuleManager().enableModules();
- }
+ plugin.getModuleManager().enableModules();
+
+ event.getSender().sendMessage(ChatUtil.color("&bRPG Addon reloaded!"));
+ }
}
diff --git a/src/main/java/com/projectkorra/rpg/RPGMethods.java b/src/main/java/com/projectkorra/rpg/RPGMethods.java
index 6d03b9b..ecbb221 100644
--- a/src/main/java/com/projectkorra/rpg/RPGMethods.java
+++ b/src/main/java/com/projectkorra/rpg/RPGMethods.java
@@ -1,14 +1,21 @@
package com.projectkorra.rpg;
import net.luckperms.api.node.Node;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Registry;
+import org.bukkit.Sound;
+import org.bukkit.boss.BarColor;
+import org.bukkit.boss.BarStyle;
import org.bukkit.entity.Player;
+import org.jetbrains.annotations.Nullable;
import java.time.Duration;
+import java.util.Arrays;
+import java.util.Locale;
import static com.projectkorra.rpg.ProjectKorraRPG.luckPermsAPI;
public class RPGMethods {
- private static final ProjectKorraRPG plugin = ProjectKorraRPG.getPlugin();
/**
* @param player Player who will lose permission
@@ -17,7 +24,7 @@ public class RPGMethods {
* @Description This method is a simplified way of removing
* Permissions to players via LuckPerms
*/
- public static void removePermission(Player player, String permission) {
+ public static void removePermission(final Player player, final String permission) {
if (luckPermsAPI == null)
return;
@@ -34,25 +41,62 @@ public static void removePermission(Player player, String permission) {
* @Description This method is a simplified way of adding
* Permissions to players via LuckPerms
*/
- public static void addPermission(Player player, String permission) {
+ public static void addPermission(final Player player, final String permission) {
if (luckPermsAPI == null)
return;
+
luckPermsAPI.getUserManager().getUser(player.getUniqueId()).data()
.add(Node.builder(permission).build());
luckPermsAPI.getUserManager().saveUser(luckPermsAPI.getUserManager().getUser(player.getUniqueId()));
}
+ public static BarColor convertStringToBarColor(final String colorStr) {
+ if (colorStr == null) {
+ return BarColor.RED;
+ }
+
+ try {
+ return BarColor.valueOf(colorStr.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException exception) {
+ ProjectKorraRPG.getPlugin().getLogger().warning("Failed BarColor conversion for String: '" + colorStr + "'! Possible values: " + Arrays.toString(BarColor.values()));
+ return null;
+ }
+ }
+
+ public static BarStyle convertStringToBarStyle(final String styleStr) {
+ if (styleStr == null) {
+ return BarStyle.SOLID;
+ }
+
+ try {
+ return BarStyle.valueOf(styleStr.toUpperCase(Locale.ROOT));
+ } catch (IllegalArgumentException exception) {
+ ProjectKorraRPG.getPlugin().getLogger().warning("Failed BarColor conversion for String: '" + styleStr + "'! Possible values: " + Arrays.toString(BarStyle.values()));
+ return null;
+ }
+ }
+
+ public static @Nullable Sound resolveSound(final String raw) {
+ if (raw == null) return null;
+ String soundId = raw.trim().toLowerCase(Locale.ROOT);
+ if (soundId.isEmpty()) return null;
+
+ NamespacedKey key = soundId.contains(":") ? NamespacedKey.fromString(soundId) : NamespacedKey.minecraft(soundId);
+
+ return (key == null) ? null : Registry.SOUNDS.get(key);
+ }
+
/**
* @param period String to convert to duration
* @return Duration in the period string
* @author CrashCringle
* @Description This method converts a period string like 3d4h to a duration object
*/
- public static Duration periodStringToDuration(String period) {
+ public static Duration periodStringToDuration(final String period) {
// Can be in the formats like: 1s, 1m, 1h, 1d, 2d1h10s etc etc.
Duration duration = Duration.ZERO;
if (period == null || period.isEmpty()) {
- plugin.getLogger().info("Invalid period string: " + period);
+ ProjectKorraRPG.getPlugin().getLogger().info("Invalid period string: " + period);
return duration;
}
String[] parts = period.split("(?<=\\D)(?=\\d)");
diff --git a/src/main/java/com/projectkorra/rpg/configuration/ConfigManager.java b/src/main/java/com/projectkorra/rpg/configuration/ConfigManager.java
index eb4493d..314f53f 100644
--- a/src/main/java/com/projectkorra/rpg/configuration/ConfigManager.java
+++ b/src/main/java/com/projectkorra/rpg/configuration/ConfigManager.java
@@ -3,11 +3,13 @@
import com.projectkorra.projectkorra.Element;
import com.projectkorra.projectkorra.configuration.ConfigType;
import org.bukkit.Material;
-import org.bukkit.Sound;
import org.bukkit.configuration.file.FileConfiguration;
import java.io.File;
-import java.util.*;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
public class ConfigManager {
private static final ConfigType DEFAULT = new ConfigType("Default");
@@ -239,18 +241,16 @@ public void configCheck(ConfigType type) {
} else if (type == WORLDEVENTS) {
config = sozinsCometConfig.get();
- List disabledWorlds = new ArrayList<>();
- disabledWorlds.add("none");
-
- config.addDefault("Title", "&cSozins Comet");
+ config.addDefault("Title", "&cSozin's Comet");
config.addDefault("Duration", 5000);
- config.addDefault("World", "world");
+
+ config.addDefault("Worlds", List.of("world"));
config.addDefault("Schedule.At", "7am");
config.addDefault("Schedule.Repeat", "7d");
config.addDefault("Schedule.Calendar", "REALTIME");
config.addDefault("Schedule.Offset", "3d12h");
- config.addDefault("Schedule.TriggerChance", "0.1");
+ config.addDefault("Schedule.TriggerChance", 0.1);
config.addDefault("Schedule.Cooldown", "60d");
config.addDefault("DisplayMethods.BossBar.Enabled", true);
@@ -258,18 +258,19 @@ public void configCheck(ConfigType type) {
config.addDefault("DisplayMethods.BossBar.Style", "SOLID");
config.addDefault("DisplayMethods.BossBar.Smooth", true);
config.addDefault("DisplayMethods.Chat.Enabled", true);
- config.addDefault("DisplayMethods.Chat.EventStartMessage", "&cSozins Comet has entered the world's atmosphere. Firebenders bending has been extremely hightened");
- config.addDefault("DisplayMethods.Chat.EventStopMessage", "&cSozins Comet has left the world's atmosphere. Firebenders bending has been normalized");
- config.addDefault("DisplayMethods.ScoreBoard.Enabled", false);
+ config.addDefault("DisplayMethods.Chat.EventStartMessage", "&cSozin's Comet has entered the world's atmosphere. Fire benders bending has been extremely heightened!");
+ config.addDefault("DisplayMethods.Chat.EventStopMessage", "&cSozin's Comet has left the world's atmosphere. Fire benders bending has been normalized!");
+ config.addDefault("DisplayMethods.Chat.EventCurrentlyRunning", "&cSozin's Comet is currently in the world's atmosphere. Fire benders bending is extremely heightened!");
+ config.addDefault("DisplayMethods.Scoreboard.Enabled", false);
config.addDefault("PlayEventStartSound", true);
- config.addDefault("EventStart.Sound", Sound.ENTITY_ENDER_DRAGON_GROWL.toString());
- config.addDefault("EventStart.Volume", "1F");
- config.addDefault("EventStart.Pitch", "0.5F");
+ config.addDefault("EventStart.Sound", "entity.ender_dragon.growl");
+ config.addDefault("EventStart.Volume", 1);
+ config.addDefault("EventStart.Pitch", 0.5);
config.addDefault("PlayEventStopSound", true);
- config.addDefault("EventStop.Sound", Sound.ENTITY_ENDER_DRAGON_AMBIENT.toString());
- config.addDefault("EventStop.Volume", "1F");
- config.addDefault("EventStop.Pitch", "0.5F");
- config.addDefault("DisabledWorlds", disabledWorlds);
+ config.addDefault("EventStop.Sound", "entity.ender_dragon.growl");
+ config.addDefault("EventStop.Volume", 1);
+ config.addDefault("EventStop.Pitch", 0.5);
+ config.addDefault("DisabledWorlds", List.of("none"));
config.addDefault("Abilities.Fire._All.Damage", "x2.0");
config.addDefault("Abilities.Fire._All.Speed", "x2.0");
config.addDefault("Abilities.Fire._All.Cooldown", "x0.5");
diff --git a/src/main/java/com/projectkorra/rpg/modules/Module.java b/src/main/java/com/projectkorra/rpg/modules/Module.java
index dff53ac..2d4aa49 100644
--- a/src/main/java/com/projectkorra/rpg/modules/Module.java
+++ b/src/main/java/com/projectkorra/rpg/modules/Module.java
@@ -5,10 +5,11 @@
import org.bukkit.event.Listener;
public abstract class Module {
- private final ProjectKorraRPG plugin = ProjectKorraRPG.getPlugin();
+ private final ProjectKorraRPG plugin;
private final String name;
- public Module(String name) {
+ public Module(ProjectKorraRPG plugin, String name) {
+ this.plugin = plugin;
this.name = name;
}
@@ -18,7 +19,7 @@ public Module(String name) {
public void registerListeners(Listener... l) {
for (Listener listener : l) {
- ProjectKorraRPG.getPlugin().getServer().getPluginManager().registerEvents(listener, ProjectKorraRPG.getPlugin());
+ this.plugin.getServer().getPluginManager().registerEvents(listener, this.plugin);
}
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/ModuleManager.java b/src/main/java/com/projectkorra/rpg/modules/ModuleManager.java
index d8f2f79..ffff375 100644
--- a/src/main/java/com/projectkorra/rpg/modules/ModuleManager.java
+++ b/src/main/java/com/projectkorra/rpg/modules/ModuleManager.java
@@ -1,40 +1,59 @@
package com.projectkorra.rpg.modules;
+import com.projectkorra.rpg.ProjectKorraRPG;
import com.projectkorra.rpg.modules.elementassignments.ElementAssignModule;
import com.projectkorra.rpg.modules.leveling.LevelingModule;
import com.projectkorra.rpg.modules.randomavatar.AvatarCycleModule;
import com.projectkorra.rpg.modules.worldevents.WorldEventModule;
-import java.util.ArrayList;
import java.util.List;
public class ModuleManager {
- private final List modules = new ArrayList<>();
+ private final ProjectKorraRPG plugin;
+ private final List modules;
private final WorldEventModule worldEventModule;
private final LevelingModule levelingModuleModule;
private final AvatarCycleModule avatarCycleModule;
private final ElementAssignModule elementAssignModule;
- public ModuleManager() {
- modules.add(worldEventModule = new WorldEventModule());
- modules.add(levelingModuleModule = new LevelingModule());
- modules.add(avatarCycleModule = new AvatarCycleModule());
- modules.add(elementAssignModule = new ElementAssignModule());
+ public ModuleManager(ProjectKorraRPG plugin) {
+ this.plugin = plugin;
+
+ this.worldEventModule = new WorldEventModule(plugin);
+ this.levelingModuleModule = new LevelingModule(plugin);
+ this.avatarCycleModule = new AvatarCycleModule(plugin);
+ this.elementAssignModule = new ElementAssignModule(plugin);
+
+ this.modules = List.of(
+ worldEventModule,
+ levelingModuleModule,
+ avatarCycleModule,
+ elementAssignModule
+ );
}
public void enableModules() {
- for (Module module : modules) {
- if (module.isEnabled()) {
- module.enable();
- }
- }
+ for (Module module : modules) {
+ if (module.isEnabled()) {
+ try {
+ module.enable();
+ } catch (Throwable t) {
+ plugin.getLogger().severe("[Module: " + module.getName() +"] enable failed: " + t);
+ }
+ }
+ }
}
public void disableModules() {
- for (Module module : modules) {
- module.disable();
- }
+ for (int i = modules.size() - 1; i >= 0; i--) {
+ Module module = modules.get(i);
+ try {
+ module.disable();
+ } catch (Throwable t) {
+ plugin.getLogger().severe("[Module: " + module.getName() + "] disable failed: " + t);
+ }
+ }
}
public List getModules() {
diff --git a/src/main/java/com/projectkorra/rpg/modules/elementassignments/ElementAssignModule.java b/src/main/java/com/projectkorra/rpg/modules/elementassignments/ElementAssignModule.java
index ff3624e..5fdae92 100644
--- a/src/main/java/com/projectkorra/rpg/modules/elementassignments/ElementAssignModule.java
+++ b/src/main/java/com/projectkorra/rpg/modules/elementassignments/ElementAssignModule.java
@@ -1,5 +1,6 @@
package com.projectkorra.rpg.modules.elementassignments;
+import com.projectkorra.rpg.ProjectKorraRPG;
import com.projectkorra.rpg.modules.Module;
import com.projectkorra.rpg.modules.elementassignments.listeners.AssignmentListener;
import com.projectkorra.rpg.modules.elementassignments.manager.AssignmentManager;
@@ -9,14 +10,14 @@ public class ElementAssignModule extends Module {
private AssignmentManager assignmentManager;
private AssignmentListener assignmentListener;
- public ElementAssignModule() {
- super("ElementAssignments");
+ public ElementAssignModule(ProjectKorraRPG plugin) {
+ super(plugin, "ElementAssignments");
}
@Override
public void enable() {
this.assignmentManager = new AssignmentManager();
- this.assignmentListener = new AssignmentListener();
+ this.assignmentListener = new AssignmentListener(getPlugin());
registerListeners(
this.assignmentListener
diff --git a/src/main/java/com/projectkorra/rpg/modules/elementassignments/listeners/AssignmentListener.java b/src/main/java/com/projectkorra/rpg/modules/elementassignments/listeners/AssignmentListener.java
index 497bbdc..d3d0b9d 100644
--- a/src/main/java/com/projectkorra/rpg/modules/elementassignments/listeners/AssignmentListener.java
+++ b/src/main/java/com/projectkorra/rpg/modules/elementassignments/listeners/AssignmentListener.java
@@ -14,9 +14,9 @@ public class AssignmentListener implements Listener {
private final AssignmentManager assignmentManager;
private final AvatarManager avatarManager;
- public AssignmentListener() {
- this.assignmentManager = ProjectKorraRPG.getPlugin().getModuleManager().getElementAssignmentsModule().getAssignmentManager();
- this.avatarManager = ProjectKorraRPG.getPlugin().getModuleManager().getRandomAvatarModule().getAvatarManager();
+ public AssignmentListener(ProjectKorraRPG plugin) {
+ this.assignmentManager = plugin.getModuleManager().getElementAssignmentsModule().getAssignmentManager();
+ this.avatarManager = plugin.getModuleManager().getRandomAvatarModule().getAvatarManager();
}
@EventHandler (priority = EventPriority.LOW)
diff --git a/src/main/java/com/projectkorra/rpg/modules/elementassignments/manager/AssignmentManager.java b/src/main/java/com/projectkorra/rpg/modules/elementassignments/manager/AssignmentManager.java
index 3d9ca85..be9b1d8 100644
--- a/src/main/java/com/projectkorra/rpg/modules/elementassignments/manager/AssignmentManager.java
+++ b/src/main/java/com/projectkorra/rpg/modules/elementassignments/manager/AssignmentManager.java
@@ -103,6 +103,9 @@ public void assignGroup(AssignmentGroup assignmentGroup, BendingPlayer bendingPl
ChatUtil.sendBrandingMessage(bendingPlayer.getPlayer(), element.getColor() + "You are now a " + element.getName() + "bender.");
}
+ bendingPlayer.saveElements();
+ bendingPlayer.saveSubElements();
+
for (String command : assignmentGroup.getCommandsToRun()) {
String formattedCommand = command.replace("%player%", bendingPlayer.getName());
if (bendingPlayer.isOnline()) {
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/LevelingModule.java b/src/main/java/com/projectkorra/rpg/modules/leveling/LevelingModule.java
index 266008f..4411c6f 100644
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/LevelingModule.java
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/LevelingModule.java
@@ -1,11 +1,12 @@
package com.projectkorra.rpg.modules.leveling;
+import com.projectkorra.rpg.ProjectKorraRPG;
import com.projectkorra.rpg.modules.Module;
import com.projectkorra.rpg.modules.leveling.commands.LevelCommand;
public class LevelingModule extends Module {
- public LevelingModule() {
- super("Leveling");
+ public LevelingModule(ProjectKorraRPG plugin) {
+ super(plugin, "Leveling");
}
@Override
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/commands/LevelCommand.java b/src/main/java/com/projectkorra/rpg/modules/leveling/commands/LevelCommand.java
index b5bc12e..67e01f7 100644
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/commands/LevelCommand.java
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/commands/LevelCommand.java
@@ -1,7 +1,7 @@
package com.projectkorra.rpg.modules.leveling.commands;
import com.projectkorra.rpg.commands.RPGCommand;
-import com.projectkorra.rpg.modules.leveling.gui.master.MainGui;
+import com.projectkorra.rpg.modules.leveling.gui.MainMenu;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
@@ -21,7 +21,7 @@ public void execute(CommandSender sender, List args) {
}
if (args.isEmpty()) {
- new MainGui(player).show(player);
+ new MainMenu().open(player);
} else {
help(sender, true);
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/GuiManager.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/GuiManager.java
deleted file mode 100644
index 9b388ec..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/GuiManager.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui;
-
-import com.github.stefvanschie.inventoryframework.gui.type.util.Gui;
-import com.projectkorra.rpg.ProjectKorraRPG;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class GuiManager {
- private final ProjectKorraRPG plugin;
- private final List allGuis = new ArrayList<>();
-
- public GuiManager(ProjectKorraRPG plugin) {
- this.plugin = plugin;
- }
-
- public void init() {
-
- }
-
- public ProjectKorraRPG getPlugin() {
- return plugin;
- }
-
- public List getAllGuis() {
- return allGuis;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/MainMenu.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/MainMenu.java
new file mode 100644
index 0000000..da659dd
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/MainMenu.java
@@ -0,0 +1,24 @@
+package com.projectkorra.rpg.modules.leveling.gui;
+
+import com.projectkorra.rpg.ui.InventoryUI;
+import com.projectkorra.rpg.ui.builder.InventoryUIBuilder;
+import com.projectkorra.rpg.ui.menu.Menu;
+import com.projectkorra.rpg.ui.util.ItemUtil;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class MainMenu implements Menu {
+ private static final int ROWS = 3;
+
+ @Override
+ public InventoryUI buildUI(Player player) {
+ ItemStack filler = ItemUtil.create(Material.GRAY_STAINED_GLASS_PANE, " ");
+ ItemStack vines = ItemUtil.create(Material.TWISTING_VINES, " ");
+
+ return InventoryUIBuilder.create(ROWS, "&1Main Menu")
+ .fillLeftRight(vines, filler)
+ .withButton(3, 1, ItemUtil.create(Material.NETHER_STAR, "&1Skill Tree"), click -> new SkillTreeMenu().open(player))
+ .build();
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/SkillTreeMenu.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/SkillTreeMenu.java
new file mode 100644
index 0000000..f121c02
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/SkillTreeMenu.java
@@ -0,0 +1,15 @@
+package com.projectkorra.rpg.modules.leveling.gui;
+
+import com.projectkorra.rpg.ui.InventoryUI;
+import com.projectkorra.rpg.ui.builder.InventoryUIBuilder;
+import com.projectkorra.rpg.ui.menu.Menu;
+import org.bukkit.entity.Player;
+
+public class SkillTreeMenu implements Menu {
+ private static final int ROWS = 3;
+
+ @Override
+ public InventoryUI buildUI(Player player) {
+ return InventoryUIBuilder.create(ROWS, "&6Skill Tree").build();
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuBase.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuBase.java
deleted file mode 100644
index 879367e..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuBase.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.framework;
-
-import org.bukkit.Material;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.InventoryHolder;
-import org.bukkit.inventory.ItemStack;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public abstract class MenuBase implements InventoryHolder {
- protected Map items = new HashMap<>();
- protected Inventory inventory;
- protected String title;
- protected int size;
- protected int lastClickedSlot = -1;
-
- public MenuBase(String title, int rows) {
- this.title = title;
- this.size = rows * 9;
- }
-
- public boolean addMenuItem(MenuItem item, int x, int y) {
- return addMenuItem(item, y * 9 + x);
- }
-
- public boolean addMenuItem(MenuItem item, int index) {
- if (index < 0) {
- index = getInventory().getSize() - index;
- }
-
- ItemStack slot = getInventory().getItem(index);
- if (slot != null && slot.getType() != Material.AIR) {
- return false;
- }
-
- ItemStack stack = item.getItem();
-
- if (!item.getItem().getEnchantments().isEmpty()) {
- // ADD GLOW
- }
-
- getInventory().setItem(index, stack);
- items.put(index, item);
- //item.setMenu(this);
- return true;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuItem.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuItem.java
deleted file mode 100644
index d790b29..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/framework/MenuItem.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.framework;
-
-import org.bukkit.inventory.ItemStack;
-
-public class MenuItem {
- private final ItemStack item;
- private final String name;
- private final Runnable runnable;
-
- public MenuItem(ItemStack item, String name, Runnable runnable) {
- this.item = item;
- this.name = name;
- this.runnable = runnable;
- }
-
- public ItemStack getItem() {
- return item;
- }
-
- public String getName() {
- return name;
- }
-
- public Runnable getRunnable() {
- return runnable;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AbilityMenu.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AbilityMenu.java
deleted file mode 100644
index fbb3dbe..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AbilityMenu.java
+++ /dev/null
@@ -1,159 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.master;
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem;
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
-import com.github.stefvanschie.inventoryframework.pane.StaticPane;
-import com.projectkorra.projectkorra.BendingPlayer;
-import com.projectkorra.projectkorra.Element;
-import com.projectkorra.projectkorra.ability.*;
-import com.projectkorra.projectkorra.util.ChatUtil;
-import com.projectkorra.rpg.configuration.ConfigManager;
-import com.projectkorra.rpg.modules.leveling.gui.util.GuiItems;
-import org.bukkit.Material;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-
-import java.util.Set;
-import java.util.TreeSet;
-
-public class AbilityMenu extends ChestGui {
- private static final int WIDTH = 9;
- private static final int HEIGHT = 6;
-
- private final StaticPane outlinePane;
- private final StaticPane abilityPane;
- private final Element element;
-
- public AbilityMenu(Element element) {
- super(HEIGHT, ChatUtil.color("Abilities"));
-
- this.outlinePane = new StaticPane(0, 0, WIDTH, HEIGHT);
- this.abilityPane = new StaticPane(1, 1, WIDTH - 2, HEIGHT - 2);
-
- this.element = element;
-
- setOnGlobalClick(event -> event.setCancelled(true));
-
- addPane(outlinePane);
- addPane(abilityPane);
-
- outlinePane.setVisible(true);
- abilityPane.setVisible(true);
-
- setupGui();
- }
-
- private void setupGui() {
- GuiItem vines = GuiItems.twistedVinesItem();
- GuiItem glass = GuiItems.glassPaneItem();
-
- GuiItem backIcon = new GuiItem(new ItemStack(Material.BARRIER), event -> {
- event.setCancelled(true);
- BendingPlayer bPlayer = BendingPlayer.getBendingPlayer((Player) event.getWhoClicked());
-
- if (bPlayer.getElements().size() >= 2) {
- new ElementPicker((Player) event.getWhoClicked()).show(event.getWhoClicked());
- } else {
- new MainGui(bPlayer.getPlayer()).show(event.getWhoClicked());
- }
- });
-
- GuiItem beforeIcon = new GuiItem(new ItemStack(Material.ARROW), event -> {
-
- });
-
- GuiItem nextIcon = new GuiItem(new ItemStack(Material.ARROW), event -> {
-
- });
-
- // top and bottom rows
- for (int x = 1; x < WIDTH - 1; x++) {
- outlinePane.addItem(glass, x, 0);
- outlinePane.addItem(glass, x, HEIGHT - 1);
- }
-
- // left and right columns
- for (int y = 0; y < HEIGHT; y++) {
- outlinePane.addItem(vines, 0, y);
- outlinePane.addItem(vines, WIDTH - 1, y);
- }
-
- outlinePane.addItem(beforeIcon, 3, 5);
- outlinePane.addItem(backIcon, 4, 5);
- outlinePane.addItem(nextIcon, 5, 5);
-
- Set sortedNames = new TreeSet<>();
-
- for (CoreAbility ability : CoreAbility.getAbilitiesByElement(element)) {
- if (!ability.isEnabled() || ability.isHiddenAbility()) continue;
-
- if (element == Element.FIRE && (ability instanceof LightningAbility || ability instanceof CombustionAbility))
- continue;
-
- if (element == Element.AIR && (ability instanceof FlightAbility || ability instanceof SpiritualAbility))
- continue;
-
- if (element == Element.EARTH && (ability instanceof MetalAbility || ability instanceof LavaAbility || ability instanceof SandAbility))
- continue;
-
- if (element == Element.WATER && (ability instanceof BloodAbility || ability instanceof IceAbility || ability instanceof HealingAbility || ability instanceof PlantAbility))
- continue;
-
- sortedNames.add(ability.getName());
- }
-
- for (String name : sortedNames) {
- ItemStack icon = abilityItem(name);
- abilityPane.addItem(new GuiItem(icon), 0, 0);
- }
-
- int x = 0, y = 0;
- for (String name : sortedNames) {
- if (y >= 5) break;
-
- ItemStack icon = abilityItem(name);
-
- GuiItem abilityItem = new GuiItem(icon, event -> {
- event.setCancelled(true);
- new AttributeMenu(CoreAbility.getAbility(name)).show(event.getWhoClicked());
- });
-
- abilityPane.addItem(abilityItem, x, y);
-
- x++;
- if (x >= abilityPane.getLength()) {
- x = 0;
- y++;
- }
- }
- }
-
- private ItemStack abilityItem(String name) {
- Material mat;
-
- switch (element.getName()) {
- case "Fire":
- mat = Material.getMaterial(ConfigManager.getDefaultFileConfig().getString("Modules.Leveling.Menu.FireIcon", Material.CAMPFIRE.toString()));
- break;
- case "Water":
- mat = Material.getMaterial(ConfigManager.getDefaultFileConfig().getString("Modules.Leveling.Menu.WaterIcon", Material.WATER_BUCKET.toString()));
- break;
- case "Air":
- mat = Material.getMaterial(ConfigManager.getDefaultFileConfig().getString("Modules.Leveling.Menu.AirIcon", Material.WIND_CHARGE.toString()));;
- break;
- case "Earth":
- mat = Material.getMaterial(ConfigManager.getDefaultFileConfig().getString("Modules.Leveling.Menu.EarthIcon", Material.DIRT.toString()));;
- break;
- default:
- mat = Material.AIR;
- }
-
- ItemStack item = new ItemStack(mat);
- ItemMeta meta = item.getItemMeta();
- assert meta != null;
- meta.setDisplayName(element.getColor() + name);
- item.setItemMeta(meta);
- return item;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AttributeMenu.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AttributeMenu.java
deleted file mode 100644
index 5a9106c..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/AttributeMenu.java
+++ /dev/null
@@ -1,138 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.master;
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem;
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
-import com.github.stefvanschie.inventoryframework.pane.StaticPane;
-import com.projectkorra.projectkorra.ability.CoreAbility;
-import com.projectkorra.projectkorra.attribute.Attribute;
-import com.projectkorra.projectkorra.util.ChatUtil;
-import com.projectkorra.rpg.modules.leveling.gui.util.GuiItems;
-import org.bukkit.Material;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-
-import java.lang.reflect.Field;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-public class AttributeMenu extends ChestGui {
- private static final int WIDTH = 9;
- private static final int HEIGHT = 6;
-
- private final CoreAbility coreAbility;
- private final StaticPane background;
-
- public AttributeMenu(CoreAbility coreAbility) {
- super(HEIGHT, ChatUtil.color(coreAbility.getElement().getColor() + coreAbility.getName() + " Attributes"));
- this.coreAbility = coreAbility;
- this.background = new StaticPane(0, 0, WIDTH, HEIGHT);
-
- setOnGlobalClick(event -> event.setCancelled(true));
-
- addPane(background);
-
- drawBackground();
- populatedAttributes();
- }
-
- private void drawBackground() {
- GuiItem vines = GuiItems.twistedVinesItem();
- GuiItem glass = GuiItems.glassPaneItem();
-
- GuiItem backIcon = new GuiItem(new ItemStack(Material.BARRIER), event -> {
- event.setCancelled(true);
- new AbilityMenu(coreAbility.getElement()).show(event.getWhoClicked());
- });
-
- GuiItem beforeIcon = new GuiItem(new ItemStack(Material.ARROW), event -> {
-
- });
-
- GuiItem nextIcon = new GuiItem(new ItemStack(Material.ARROW), event -> {
-
- });
-
- for (int x = 1; x < WIDTH - 1; x++) {
- background.addItem(glass, x, 0);
- background.addItem(glass, x, HEIGHT - 1);
- }
-
- for (int y = 0; y < HEIGHT; y++) {
- background.addItem(vines, 0, y);
- background.addItem(vines, WIDTH - 1, y);
- }
-
- background.addItem(beforeIcon, 3, 5);
- background.addItem(backIcon, 4, 5);
- background.addItem(nextIcon, 5, 5);
- }
-
- private void populatedAttributes() {
- List entries = new ArrayList<>();
- for (Field field : coreAbility.getClass().getDeclaredFields()) {
- if (!field.isAnnotationPresent(Attribute.class)) continue;
- field.setAccessible(true);
-
- try {
- Object value = field.get(coreAbility);
- String key = field.getAnnotation(Attribute.class).value();
- entries.add(new AttributeEntry(key, value));
- } catch (IllegalAccessException ignored) {}
- }
-
- entries.sort(Comparator.comparing(e -> e.key));
-
- int x = 1, y = 1;
- int innerWidth = WIDTH - 2;
- int innerHeight = HEIGHT - 2;
-
- for (AttributeEntry entry : entries) {
- if (y > innerHeight) break;
-
- ItemStack icon = createIcon(entry.key, entry.value);
- GuiItem guiItem = new GuiItem(icon, event -> {
- event.setCancelled(true);
- });
- background.addItem(guiItem, x, y);
-
- x = x + 2;
- if (x > innerWidth) {
- x = 1;
- y = y + 2;
- }
- }
- }
-
- private ItemStack createIcon(String attributeName, Object value) {
- Material mat = switch (attributeName.toLowerCase()) {
- case "speed" -> Material.RABBIT_FOOT;
- case "range" -> Material.ENDER_PEARL;
- case "damage" -> Material.DIAMOND;
- case "duration" -> Material.SLIME_BALL;
- case "radius" -> Material.HONEYCOMB;
- case "knockback" -> Material.STICK;
- case "fireticks" -> Material.BLAZE_POWDER;
- case "cooldown" -> Material.SUGAR;
- default -> Material.PAPER;
- };
-
- ItemStack item = new ItemStack(mat);
- ItemMeta meta = item.getItemMeta();
- if (meta != null) {
- // display attribute name & value
- meta.setDisplayName(capitalize(attributeName));
- meta.setLore(Collections.singletonList(ChatUtil.color("&7Value: &f" + value)));
- item.setItemMeta(meta);
- }
- return item;
- }
-
- private String capitalize(String str) {
- if (str == null || str.isEmpty()) return str;
- return Character.toUpperCase(str.charAt(0)) + str.substring(1).toLowerCase();
- }
-
- private record AttributeEntry(String key, Object value) {}
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/ElementPicker.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/ElementPicker.java
deleted file mode 100644
index 9edae91..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/ElementPicker.java
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.master;
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem;
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
-import com.github.stefvanschie.inventoryframework.pane.StaticPane;
-import com.projectkorra.projectkorra.BendingPlayer;
-import com.projectkorra.projectkorra.Element;
-import com.projectkorra.projectkorra.util.ChatUtil;
-import com.projectkorra.rpg.modules.leveling.gui.util.GuiItems;
-import net.md_5.bungee.api.ChatColor;
-import org.bukkit.Material;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
-
-public class ElementPicker extends ChestGui {
- private static final int WIDTH = 9;
- private static final int HEIGHT = 3;
-
- private final Player player;
- private final StaticPane background;
- private final List elements;
-
- public ElementPicker(Player player) {
- super(HEIGHT, ChatUtil.color("Select Element"));
- this.player = player;
- this.background = new StaticPane(0, 0, WIDTH, HEIGHT);
- this.elements = BendingPlayer.getBendingPlayer(player).getElements();
-
- setOnGlobalClick(event -> event.setCancelled(true));
-
- addPane(background);
- background.setVisible(true);
-
- setupGui();
- }
-
- private void setupGui() {
- // fill the background with gray glass
- background.fillWith(GuiItems.glassPaneItem().getItem());
-
- // draw twisted vines border
- GuiItem vines = GuiItems.twistedVinesItem();
- // top and bottom rows
- for (int x = 1; x < WIDTH; x++) {
- background.addItem(vines, x, 0);
- background.addItem(vines, x, HEIGHT - 1);
- }
- // left and right columns
- for (int y = 0; y < HEIGHT; y++) {
- background.addItem(vines, 0, y);
- background.addItem(vines, WIDTH - 1, y);
- }
-
- // collect and sort element items
- List items = elementItems();
-
- // place element buttons starting at (1,1)
- int paneWidth = WIDTH - 2;
- int x = 0, y = 0;
- for (ItemStack stack : items) {
-
- GuiItem guiItem = new GuiItem(stack, event -> {
- event.setCancelled(true);
- ItemStack clicked = event.getCurrentItem();
-
- assert clicked != null;
- assert clicked.getItemMeta() != null;
-
- String rawName = ChatColor.stripColor(clicked.getItemMeta().getDisplayName());
-
- Element picked = Element.getElement(rawName);
-
- new AbilityMenu(picked).show(player);
- });
- background.addItem(guiItem, x + 1, y + 1);
-
- x = x + 2;
- if (x >= paneWidth) {
- x = 0;
- }
- }
- }
-
- /**
- * Builds a list of ItemStacks for each element the player has, sorted by name.
- */
- private List elementItems() {
- List items = new ArrayList<>();
- for (Element element : elements) {
- Material mat;
- switch (element.getName()) {
- case "Fire": mat = Material.CAMPFIRE; break;
- case "Water": mat = Material.WATER_BUCKET; break;
- case "Air": mat = Material.WIND_CHARGE; break;
- case "Earth": mat = Material.DIRT; break;
- default: continue;
- }
-
- ItemStack item = new ItemStack(mat);
- ItemMeta meta = item.getItemMeta();
- if (meta != null) {
- meta.setDisplayName(element.getColor() + element.getName());
- item.setItemMeta(meta);
- }
- items.add(item);
- }
- // sort alphabetically by display name
- items.sort(Comparator.comparing(i -> Objects.requireNonNull(i.getItemMeta()).getDisplayName()));
- return items;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/MainGui.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/MainGui.java
deleted file mode 100644
index 7d3c4d9..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/master/MainGui.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.master;
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem;
-import com.github.stefvanschie.inventoryframework.gui.type.ChestGui;
-import com.github.stefvanschie.inventoryframework.pane.StaticPane;
-import com.projectkorra.projectkorra.BendingPlayer;
-import com.projectkorra.projectkorra.Element;
-import com.projectkorra.projectkorra.util.ChatUtil;
-import com.projectkorra.rpg.modules.leveling.gui.util.GuiItems;
-import net.md_5.bungee.api.ChatColor;
-import org.bukkit.Material;
-import org.bukkit.entity.Player;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-import org.bukkit.inventory.meta.SkullMeta;
-
-public class MainGui extends ChestGui {
- private static final int WIDTH = 9;
- private static final int HEIGHT = 3;
-
- private final StaticPane background;
-
- private final Player player;
-
- public MainGui(Player player) {
- super(HEIGHT, ChatUtil.color("Leveling"));
- this.player = player;
-
- setOnGlobalClick(event -> event.setCancelled(true));
-
- background = new StaticPane(0, 0, WIDTH, HEIGHT);
- background.fillWith(GuiItems.glassPaneItem().getItem());
-
- for (int y = 0; y < HEIGHT; y++) {
- background.addItem(GuiItems.twistedVinesItem(), 0, y);
- background.addItem(GuiItems.twistedVinesItem(), WIDTH - 1, y);
- }
-
- addPane(background);
-
- this.setupGui();
- }
-
- private void setupGui() {
- GuiItem shardButton = new GuiItem(amethystShardItem(), event -> {
- Player player = (Player) event.getWhoClicked();
- BendingPlayer bPlayer = BendingPlayer.getBendingPlayer(player);
-
- if (bPlayer.getElements().isEmpty()) {
- player.sendMessage("You don't have any elements yet!");
- event.setCancelled(true);
- return;
- }
-
- if (bPlayer.getElements().size() == 1) {
- Element element = bPlayer.getElements().getFirst();
- event.setCancelled(true);
- new AbilityMenu(element).show(player);
- } else {
- event.setCancelled(true);
- new ElementPicker(player).show(player);
- }
- });
-
- background.addItem(shardButton, 2, 1);
-
- GuiItem headItem = new GuiItem(getHead(), event -> {
- event.setCancelled(true);
- });
-
- background.addItem(headItem, 4, 1);
-
- GuiItem starButton = new GuiItem(netherStarItem(), event -> {
- event.setCancelled(true);
- });
-
- background.addItem(starButton, 6, 1);
- }
-
- private ItemStack amethystShardItem() {
- ItemStack amethystShard = new ItemStack(Material.AMETHYST_SHARD);
- ItemMeta amethystShardMeta = amethystShard.getItemMeta();
- assert amethystShardMeta != null;
- amethystShardMeta.setDisplayName(ChatColor.DARK_PURPLE + "" + ChatColor.ITALIC + "Attributes");
- amethystShard.setItemMeta(amethystShardMeta);
-
- return amethystShard;
- }
-
- private ItemStack getHead() {
- ItemStack item = new ItemStack(Material.PLAYER_HEAD);
- SkullMeta skull = (SkullMeta) item.getItemMeta();
- assert skull != null;
- skull.setDisplayName(player.getName());
- skull.setOwningPlayer(player);
- skull.setOwnerProfile(player.getPlayerProfile());
- item.setItemMeta(skull);
- return item;
- }
-
- private ItemStack netherStarItem() {
- ItemStack netherStar = new ItemStack(Material.NETHER_STAR);
- ItemMeta netherStarMeta = netherStar.getItemMeta();
- assert netherStarMeta != null;
- netherStarMeta.setDisplayName(ChatColor.AQUA + "" + ChatColor.ITALIC + "Skilltree");
- netherStar.setItemMeta(netherStarMeta);
- return netherStar;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/util/GuiItems.java b/src/main/java/com/projectkorra/rpg/modules/leveling/gui/util/GuiItems.java
deleted file mode 100644
index 12dea09..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/gui/util/GuiItems.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.projectkorra.rpg.modules.leveling.gui.util;
-
-import com.github.stefvanschie.inventoryframework.gui.GuiItem;
-import org.bukkit.Material;
-import org.bukkit.inventory.ItemStack;
-import org.bukkit.inventory.meta.ItemMeta;
-
-public class GuiItems {
- public static GuiItem glassPaneItem() {
- ItemStack glassPane = new ItemStack(Material.GRAY_STAINED_GLASS_PANE);
- ItemMeta glassPaneMeta = glassPane.getItemMeta();
- assert glassPaneMeta != null;
- glassPaneMeta.setDisplayName(" ");
- glassPane.setItemMeta(glassPaneMeta);
- return new GuiItem(glassPane);
- }
-
- public static GuiItem twistedVinesItem() {
- ItemStack vine = new ItemStack(Material.TWISTING_VINES);
- ItemMeta vineMeta = vine.getItemMeta();
- assert vineMeta != null;
- vineMeta.setDisplayName(" ");
- vine.setItemMeta(vineMeta);
- return new GuiItem(vine);
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/manager/RpgPlayerManager.java b/src/main/java/com/projectkorra/rpg/modules/leveling/manager/RpgPlayerManager.java
new file mode 100644
index 0000000..e44b58b
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/manager/RpgPlayerManager.java
@@ -0,0 +1,5 @@
+package com.projectkorra.rpg.modules.leveling.manager;
+
+public class RpgPlayerManager {
+
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/leveling/rpgplayer/RpgPlayer.java b/src/main/java/com/projectkorra/rpg/modules/leveling/rpgplayer/RpgPlayer.java
index 4f59a8c..3695cf0 100644
--- a/src/main/java/com/projectkorra/rpg/modules/leveling/rpgplayer/RpgPlayer.java
+++ b/src/main/java/com/projectkorra/rpg/modules/leveling/rpgplayer/RpgPlayer.java
@@ -6,28 +6,7 @@
import java.util.UUID;
-public class RpgPlayer {
- private final UUID uuid;
- private final int level;
- private final double xp;
-
- public RpgPlayer(UUID uuid, int level, double xp) {
- this.uuid = uuid;
- this.level = level;
- this.xp = xp;
- }
-
- public UUID getUuid() {
- return uuid;
- }
-
- public int getLevel() {
- return this.level;
- }
-
- public double getXp() {
- return this.xp;
- }
+public record RpgPlayer(UUID uuid, int level, double xp) {
private Player asPlayer() {
return Bukkit.getPlayer(uuid);
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/AvatarCycleModule.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/AvatarCycleModule.java
index c9bb204..9fed85c 100644
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/AvatarCycleModule.java
+++ b/src/main/java/com/projectkorra/rpg/modules/randomavatar/AvatarCycleModule.java
@@ -1,46 +1,44 @@
package com.projectkorra.rpg.modules.randomavatar;
+import com.projectkorra.rpg.ProjectKorraRPG;
import com.projectkorra.rpg.modules.Module;
+import com.projectkorra.rpg.modules.randomavatar.commands.AvatarCommand;
import com.projectkorra.rpg.modules.randomavatar.listeners.AvatarListener;
import com.projectkorra.rpg.modules.randomavatar.manager.AvatarManager;
+import org.bukkit.Bukkit;
+import org.bukkit.event.HandlerList;
public class AvatarCycleModule extends Module {
private AvatarManager avatarManager;
private AvatarListener avatarListener;
- public AvatarCycleModule() {
- super("RandomAvatar");
+ public AvatarCycleModule(ProjectKorraRPG plugin) {
+ super(plugin, "RandomAvatar");
}
@Override
public void enable() {
-// this.avatarManager = new AvatarManager();
-// this.avatarListener = new AvatarListener();
-//
-// new AvatarCommand(this.avatarManager);
-//
-// registerListeners(
-// this.avatarListener
-// );
-
-// avatarManager.refreshRecentPlayersAsync();
-//
-// Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(ProjectKorraRPG.getPlugin(), () -> {
-// ProjectKorraRPG.getPlugin().getLogger().info("Avatar selection: Checking for new avatars.");
-// avatarManager.checkAvatars();
-// }, 0L, 20L * 30); // Every 30s (For Testing)
-
-// new com.projectkorra.rpg.modules.randomavatar.avatar.AvatarManager();
-// new AvatarCycleSchedule(getPlugin());
+ this.avatarManager = new AvatarManager();
+ this.avatarListener = new AvatarListener();
+
+ new AvatarCommand(this.avatarManager);
+
+ registerListeners(
+ this.avatarListener
+ );
+
+ avatarManager.refreshRecentPlayersAsync();
+
+ Bukkit.getServer().getScheduler().scheduleSyncRepeatingTask(ProjectKorraRPG.getPlugin(), () -> avatarManager.checkAvatars(), 0L, 20L * 30); // Every 30s (For Testing)
}
@Override
public void disable() {
-// if (this.avatarListener != null) {
-// HandlerList.unregisterAll(this.avatarListener);
-// this.avatarListener = null;
-// }
+ if (this.avatarListener != null) {
+ HandlerList.unregisterAll(this.avatarListener);
+ this.avatarListener = null;
+ }
}
public AvatarManager getAvatarManager() {
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/Avatar.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/Avatar.java
deleted file mode 100644
index 54feb71..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/Avatar.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package com.projectkorra.rpg.modules.randomavatar.avatar;
-
-import com.projectkorra.projectkorra.BendingPlayer;
-import com.projectkorra.projectkorra.Element;
-import com.projectkorra.rpg.util.ChatUtil;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-
-public class Avatar {
- private final UUID uuid;
- private final Element mainElement;
- private final List subElements;
-
- private Instant chosenTime;
-
- public Avatar(UUID uuid, Element mainElement, List subElements) {
- this.uuid = uuid;
- this.mainElement = mainElement;
- this.subElements = new ArrayList<>(); // Init empty List in case of no sub elements
- this.chosenTime = Instant.now();
-
- if (!subElements.isEmpty()) {
- this.subElements.addAll(subElements);
- }
- }
-
- public Avatar(UUID uuid, Element mainElement, List subElements, Instant chosenTime) {
- this(uuid, mainElement, subElements);
-
- this.chosenTime = chosenTime;
- }
-
- public void handleInitiation() {
- Player player = Bukkit.getPlayer(uuid);
-
- if (player == null) {
- throw new NullPointerException("Couldn't initiate new Avatar! Player is null!");
- }
-
- BendingPlayer bendingPlayer = BendingPlayer.getBendingPlayer(player);
-
- if (bendingPlayer == null) {
- throw new NullPointerException("Couldn't initiate new Avatar! BendingPlayer is null!");
- }
-
- Element element = bendingPlayer.getElements().getFirst();
- List subElement = bendingPlayer.getSubElements();
- Instant chosenTime = Instant.now();
-
- if (element == null || subElement == null || chosenTime == null) {
- throw new NullPointerException("Couldn't initiate new Avatar! Element, SubElement or ChosenTime is null!");
- }
-
- AvatarManager.storeAvatar(player.getUniqueId(), element, subElement, chosenTime);
-
- removeAllElements(bendingPlayer);
- addAvatarElements(bendingPlayer);
-
- ChatUtil.sendBrandingMessage(player, Element.AVATAR.getColor() + "You have been chose as Avatar! Restore the balance in this world and bring peace!");
- }
-
- private void removeAllElements(BendingPlayer bendingPlayer) {
- if (!bendingPlayer.getElements().isEmpty()) {
- bendingPlayer.getElements().clear();
-
- if (!bendingPlayer.getSubElements().isEmpty()) {
- bendingPlayer.getSubElements().clear();
- }
- }
-
- if (bendingPlayer.hasTempElements()) {
- bendingPlayer.getTempElements().clear();
-
- if (!bendingPlayer.getTempSubElements().isEmpty()) {
- bendingPlayer.getTempSubElements().clear();
- }
- }
- }
-
- private void addAvatarElements(BendingPlayer bendingPlayer) {
- for (Element mainElements : new Element[]{Element.FIRE, Element.WATER, Element.EARTH, Element.AIR}) {
- bendingPlayer.addElement(mainElements);
- bendingPlayer.saveElements();
- }
- }
-
- public void handleDeath(boolean inAvatarState) {
-
- }
-
- public PreviousAvatar getPreviousAvatar(UUID previousAvatarUUID) {
- return AvatarManager.getPreviousAvatars().get(previousAvatarUUID) != null ? AvatarManager.getPreviousAvatars().get(previousAvatarUUID) : null;
- }
-
- public UUID getUuid() {
- return uuid;
- }
-
- public Element getMainElement() {
- return mainElement;
- }
-
- public List getSubElements() {
- return subElements;
- }
-
- public Instant getChosenTime() {
- return chosenTime;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/AvatarManager.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/AvatarManager.java
deleted file mode 100644
index cb2e31c..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/AvatarManager.java
+++ /dev/null
@@ -1,158 +0,0 @@
-package com.projectkorra.rpg.modules.randomavatar.avatar;
-
-import com.projectkorra.projectkorra.BendingPlayer;
-import com.projectkorra.projectkorra.Element;
-import com.projectkorra.projectkorra.storage.DBConnection;
-import com.projectkorra.rpg.RPGMethods;
-import com.projectkorra.rpg.configuration.ConfigManager;
-import com.projectkorra.rpg.storage.TableCreator;
-import org.bukkit.Bukkit;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.entity.Player;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.util.*;
-import java.util.stream.Collectors;
-
-public class AvatarManager {
- private static final FileConfiguration defaultConfig = ConfigManager.defaultConfig.get();
- private static final String CONFIG_PATH = "Modules.RandomAvatar.";
-
- private static HashMap CURRENT_AVATARS;
- private static HashMap PREVIOUS_AVATARS;
-
- public AvatarManager() {
- CURRENT_AVATARS = fillAvatarMap();
- PREVIOUS_AVATARS = fillPreviousAvatarMap();
- }
-
- public static boolean isEligableToBecomeAvatar(Player player) {
- if (player == null) {
- return false;
- }
-
- int maxAllowedAvatars = defaultConfig.getInt(CONFIG_PATH + "MaxAvatars", 1);
- Instant timeSinceLoginRequired = Instant.ofEpochMilli(RPGMethods.periodStringToDuration(defaultConfig.getString(CONFIG_PATH + "TimeSinceLoginRequired")).toMillis());
-
- if (CURRENT_AVATARS.size() >= maxAllowedAvatars) {
- System.out.println("Size limit reached");
- return false;
- }
-
- if (CURRENT_AVATARS.containsKey(player.getUniqueId())) {
- System.out.println("Already is avatar");
- return false;
- }
-
- if (player.getFirstPlayed() < timeSinceLoginRequired.toEpochMilli()) {
- System.out.println("Not played long enough");
- return false;
- }
-
- return true;
- }
-
- public static void storeAvatar(UUID uuid, Element mainElement, List subElements, Instant chosenTime) {
- String subElementsStr = subElements.stream().map(Element::getName).collect(Collectors.joining(","));
-
- String query = "INSERT INTO " + TableCreator.RPG_AVATAR_TABLE + "(uuid, main_element, sub_elements, chosen_time) VALUES ('" +
- uuid.toString() + "', '" +
- mainElement.getName() + "', '" +
- subElementsStr + "', '" +
- Timestamp.from(chosenTime) + "')";
-
- DBConnection.sql.modifyQuery(query);
-
- CURRENT_AVATARS.put(uuid, new Avatar(uuid, mainElement, subElements, chosenTime));
- }
-
- /**
- * Temp method
- */
- public static void checkAvatars() {
- List candidates = Bukkit.getOnlinePlayers().stream()
- .filter(AvatarManager::isEligableToBecomeAvatar)
- .collect(Collectors.toList());
-
- if (candidates.isEmpty()) {
- return;
- }
-
- Collections.shuffle(candidates);
- Player player = candidates.getFirst();
- BendingPlayer bendingPlayer = BendingPlayer.getBendingPlayer(player);
-
- if (bendingPlayer != null) {
- new Avatar(player.getUniqueId(), bendingPlayer.getElements().getFirst(), bendingPlayer.getSubElements()).handleInitiation();
- }
- }
-
- private HashMap fillAvatarMap() {
- HashMap avatars = new HashMap<>();
-
- try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT * FROM " + TableCreator.RPG_AVATAR_TABLE);
-
- while (rs.next()) {
- UUID uuid = UUID.fromString(rs.getString("uuid"));
- String mainElementName = rs.getString("main_element");
- String subElementNames = rs.getString("sub_elements");
- Instant chosenTime = rs.getTimestamp("chosen_time").toInstant();
-
- List subElements = new ArrayList<>();
- if (subElementNames != null) {
- for (String subElementName : subElementNames.split(",")) {
- subElements.add((Element.SubElement) Element.fromString(subElementName));
- }
- }
-
- avatars.put(uuid, new Avatar(uuid, Element.fromString(mainElementName), subElements, chosenTime));
- }
- } catch (SQLException e) {
- throw new RuntimeException(e);
- }
-
- return avatars;
- }
-
- private HashMap fillPreviousAvatarMap() {
- HashMap previousAvatars = new HashMap<>();
-
- try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT * FROM " + TableCreator.RPG_PASTLIVES_TABLE);
-
- while (rs.next()) {
- UUID uuid = UUID.fromString(rs.getString("uuid"));
- String mainElementName = rs.getString("main_element");
- String subElementNames = rs.getString("sub_elements");
- Instant chosenTime = rs.getTimestamp("chosen_time").toInstant();
- Instant endTime = rs.getTimestamp("end_time").toInstant();
- String endReason = rs.getString("end_reason");
-
- List subElements = new ArrayList<>();
- if (subElementNames != null) {
- for (String subElementName : subElementNames.split(",")) {
- subElements.add((Element.SubElement) Element.fromString(subElementName));
- }
- }
-
- previousAvatars.put(uuid, new PreviousAvatar(uuid, Element.fromString(mainElementName), subElements, chosenTime, endTime, endReason));
- }
- } catch (SQLException e) {
- throw new RuntimeException(e);
- }
-
- return previousAvatars;
- }
-
- public static HashMap getCurrentAvatars() {
- return CURRENT_AVATARS;
- }
-
- public static HashMap getPreviousAvatars() {
- return PREVIOUS_AVATARS;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/PreviousAvatar.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/PreviousAvatar.java
deleted file mode 100644
index b0eb1ac..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/avatar/PreviousAvatar.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.projectkorra.rpg.modules.randomavatar.avatar;
-
-import com.projectkorra.projectkorra.Element;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.UUID;
-
-public class PreviousAvatar extends Avatar {
- private final Instant endTime;
- private final String endReason;
-
- public PreviousAvatar(UUID uuid, Element mainElement, List subElements, Instant chosenTime, Instant endTime, String endReason) {
- super(uuid, mainElement, subElements, chosenTime);
- this.endTime = endTime;
- this.endReason = endReason;
- }
-
- public Instant getEndTime() {
- return endTime;
- }
-
- public String getEndReason() {
- return endReason;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/manager/AvatarManager.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/manager/AvatarManager.java
index 69dfc35..56638b3 100644
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/manager/AvatarManager.java
+++ b/src/main/java/com/projectkorra/rpg/modules/randomavatar/manager/AvatarManager.java
@@ -17,12 +17,8 @@
import org.bukkit.OfflinePlayer;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
-import org.bukkit.plugin.java.JavaPlugin;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Timestamp;
+import java.sql.*;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
@@ -31,7 +27,7 @@
import java.util.stream.Collectors;
public class AvatarManager {
- private final JavaPlugin plugin = ProjectKorraRPG.getPlugin();
+ private final ProjectKorraRPG plugin = ProjectKorraRPG.getPlugin();
// In-memory caches
public final Set recentPlayers;
@@ -93,7 +89,8 @@ public AvatarManager() {
private void grantTempElements(OfflinePlayer p, Instant startTime) {
long remaining = avatarDuration.toMillis() - Duration.between(startTime, Instant.now()).toMillis();
- BendingPlayer bp = BendingPlayer.getBendingPlayer(p);
+ OfflineBendingPlayer bp = new OfflineBendingPlayer(p);
+
avatarElements.stream()
.filter(el -> !bp.hasElement(el) && !bp.hasTempElement(el))
.forEach(el -> Bukkit.getScheduler().runTaskLater(plugin,
@@ -278,20 +275,21 @@ public boolean isCurrentRPGAvatar(UUID uuid) {
* @return if player with uuid has been the avatar
*/
public boolean hasBeenAvatar(UUID uuid) {
- if (isCurrentRPGAvatar(uuid))
- return true;
- ResultSet rs = DBConnection.sql.readQuery("SELECT uuid FROM " + TableCreator.RPG_PASTLIVES_TABLE + " WHERE uuid = '" + uuid.toString() + "'");
- boolean valid;
- try {
- valid = rs.next();
- Statement stmt = rs.getStatement();
- rs.close();
- stmt.close();
- } catch (SQLException e) {
- plugin.getLogger().severe("Error checking past avatar: " + e.getMessage());
- valid = false;
+ if (isCurrentRPGAvatar(uuid)) return true;
+
+ final String sql = "SELECT uuid FROM " + TableCreator.RPG_PAST_LIVES_TABLE + " WHERE uuid = ?";
+
+ try (Connection connection = DBConnection.sql.getConnection();
+ PreparedStatement ps = connection.prepareStatement(sql)) {
+ ps.setString(1, uuid.toString());
+
+ try (ResultSet rs = ps.executeQuery()) {
+ return rs.next();
+ }
+ } catch (SQLException exception) {
+ plugin.getLogger().severe("Error checking past avatar: " + exception.getMessage());
+ return false;
}
- return valid;
}
public boolean isAvatarEligible(UUID uuid) {
@@ -303,38 +301,42 @@ public boolean isAvatarEligible(UUID uuid) {
return false;
BendingPlayer bPlayer = BendingPlayer.getBendingPlayer(Bukkit.getOfflinePlayer(uuid));
- if (bPlayer == null)
- return false;
+ if (bPlayer == null) return false;
+
// Check if they have played in the last timeSinceLogonRequired hours
if (!bPlayer.isOnline() && (bPlayer.getPlayer().getLastPlayed() <= System.currentTimeMillis() - timeSinceLogonRequired.toMillis())) {
return false;
}
- // Check if they have been avatar recently
- try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT endTime FROM " + TableCreator.RPG_PASTLIVES_TABLE + " WHERE uuid = '" + uuid + "' ORDER BY startTime DESC LIMIT 1");
- if (rs.next()) {
- Timestamp endTime = rs.getTimestamp("endTime");
- if (endTime != null && endTime.toInstant().plus(repeatSelectionCooldown).isAfter(Instant.now())) {
- Statement stmt = rs.getStatement();
- rs.close();
- stmt.close();
- return false;
+
+ final String sql = "SELECT endTime FROM " + TableCreator.RPG_PAST_LIVES_TABLE + " WHERE uuid = ? ORDER BY startTime DESC LIMIT 1";
+
+ try (Connection connection = DBConnection.sql.getConnection();
+ PreparedStatement ps = connection.prepareStatement(sql)) {
+ ps.setString(1, uuid.toString());
+
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ Timestamp endTime = rs.getTimestamp("endTime");
+ if (endTime != null) {
+ Instant eligibleAt = endTime.toInstant().plus(repeatSelectionCooldown);
+ if (eligibleAt.isAfter(Instant.now())) {
+ return false;
+ }
+ }
}
}
- Statement stmt = rs.getStatement();
- rs.close();
- stmt.close();
- } catch (SQLException e) {
- plugin.getLogger().severe("Error checking past avatar: " + e.getMessage());
+ } catch (SQLException exception) {
+ plugin.getLogger().severe("Error checking past avatar: " + exception.getMessage());
return false;
}
+
return true;
}
private Set fetchIneligiblePastAvatars() {
Set set = new HashSet<>();
try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT uuid, MAX(endTime) AS lastEnd FROM " + TableCreator.RPG_PASTLIVES_TABLE + " GROUP BY uuid");
+ ResultSet rs = DBConnection.sql.readQuery("SELECT uuid, MAX(endTime) AS lastEnd FROM " + TableCreator.RPG_PAST_LIVES_TABLE + " GROUP BY uuid");
while (rs.next()) {
UUID uuid = UUID.fromString(rs.getString("uuid"));
Instant lastEnd = rs.getTimestamp("lastEnd").toInstant();
@@ -365,36 +367,42 @@ private void revokeRPGAvatar(UUID uuid, RemovalReason reason) {
List originalSubElements = new ArrayList<>();
Instant start = Instant.now();
- try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT * FROM " + TableCreator.RPG_AVATAR_TABLE + " WHERE uuid = '" + uuid + "'");
- if (rs.next()) {
- start = rs.getTimestamp("startTime").toInstant();
- String elements = rs.getString("elements");
- for (String elementName : elements.split(",")) {
- Element element = Element.getElement(elementName);
- if (element != null) {
- if (element instanceof Element.SubElement) {
- originalSubElements.add((Element.SubElement) element);
+ final String selectSql = "SELECT * FROM " + TableCreator.RPG_AVATAR_TABLE + " WHERE uuid = ?";
+
+ try (Connection connection = DBConnection.sql.getConnection();
+ PreparedStatement ps = connection.prepareStatement(selectSql)) {
+ ps.setString(1, uuid.toString());
+
+ try (ResultSet rs = ps.executeQuery()) {
+ if (rs.next()) {
+ start = rs.getTimestamp("startTime").toInstant();
+ String elements = rs.getString("elements");
+ for (String elementName : elements.split(",")) {
+ Element element = Element.getElement(elementName);
+ if (element != null) {
+ if (element instanceof Element.SubElement sub) {
+ originalSubElements.add(sub);
+ }
+ originalElements.add(element);
}
- originalElements.add(element);
}
}
- Statement stmt = rs.getStatement();
- rs.close();
- stmt.close();
}
- } catch (SQLException e) {
- throw new RuntimeException(e);
+ } catch (SQLException exception) {
+ throw new RuntimeException(exception);
}
- // Delete
- try {
- DBConnection.sql.getConnection().setAutoCommit(false);
- DBConnection.sql.modifyQuery("DELETE FROM " + TableCreator.RPG_AVATAR_TABLE + " WHERE uuid = '" + uuid + "'");
- DBConnection.sql.getConnection().commit();
- DBConnection.sql.getConnection().setAutoCommit(true);
- } catch (SQLException ex) {
- throw new RuntimeException(ex);
+ final String deleteSql = "DELETE FROM" + TableCreator.RPG_AVATAR_TABLE + " WHERE uuid = ?";
+
+ try (Connection connection = DBConnection.sql.getConnection();
+ PreparedStatement ps = connection.prepareStatement(deleteSql)) {
+ connection.setAutoCommit(false);
+ ps.setString(1, uuid.toString());
+ ps.executeUpdate();
+ connection.commit();
+ connection.setAutoCommit(true);
+ } catch (SQLException exception) {
+ throw new RuntimeException(exception);
}
// Restore perms and send message
@@ -433,12 +441,20 @@ private void revokeRPGAvatar(UUID uuid, RemovalReason reason) {
plugin.getLogger().info(offlinePlayer.getName() + " is no longer the Avatar.");
avatars.remove(offlinePlayer);
- // Record past life
- try {
- DBConnection.sql.modifyQuery("INSERT INTO " + TableCreator.RPG_PASTLIVES_TABLE + " (uuid, startTime, player, endTime, elements, endReason) VALUES ('" + uuid + "', '" + Timestamp.from(start) + "', '" + offlinePlayer.getName() + "', '" + Timestamp.from(Instant.now()) + "', '" + String.join(",", originalElements.stream().map(Element::getName).toArray(String[]::new)) + "', '" + reason + "')", false);
- DBConnection.sql.getConnection().setAutoCommit(true);
- } catch (SQLException ex) {
- plugin.getLogger().severe("Error recording past life: " + ex.getMessage());
+ final String insertSql = "INSERT INTO " + TableCreator.RPG_PAST_LIVES_TABLE + " (uuid, startTime, player, endTime, elements, endReason) VALUES (?, ?, ?, ?, ?, ?)";
+
+ try (Connection connection = DBConnection.sql.getConnection();
+ PreparedStatement ps = connection.prepareStatement(insertSql)) {
+ ps.setString(1, uuid.toString());
+ ps.setTimestamp(2, Timestamp.from(start));
+ ps.setString(3, offlinePlayer.getName());
+ ps.setTimestamp(4, Timestamp.from(Instant.now()));
+ ps.setString(5, String.join(",", originalElements.stream().map(Element::getName).toArray(String[]::new)));
+ ps.setString(6, reason.name());
+
+ ps.executeUpdate();
+ } catch (SQLException exception) {
+ plugin.getLogger().severe("Error recording past life: " + exception.getMessage());
}
}
@@ -464,7 +480,7 @@ public List getPastLives() {
// Past lives
try {
- ResultSet rs = DBConnection.sql.readQuery("SELECT * FROM " + TableCreator.RPG_PASTLIVES_TABLE + " ORDER BY startTime DESC");
+ ResultSet rs = DBConnection.sql.readQuery("SELECT * FROM " + TableCreator.RPG_PAST_LIVES_TABLE + " ORDER BY startTime DESC");
while (rs.next()) {
String player = rs.getString("player");
String elems = rs.getString("elements");
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/schedule/AvatarCycleSchedule.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/schedule/AvatarCycleSchedule.java
deleted file mode 100644
index a6f0584..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/randomavatar/schedule/AvatarCycleSchedule.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.projectkorra.rpg.modules.randomavatar.schedule;
-
-import com.projectkorra.rpg.ProjectKorraRPG;
-
-public class AvatarCycleSchedule {
- private final ProjectKorraRPG plugin;
-
- public AvatarCycleSchedule(ProjectKorraRPG plugin) {
- this.plugin = plugin;
-
- this.startSchedule();
- }
-
- private void startSchedule() {
- // CAN BE DONE WITHOUT TASK TIMER, CHECK FOR DEATH EVENTS AND SIMILAR CONDITIONS
-// new BukkitRunnable() {
-// @Override
-// public void run() {
-// AvatarManager.checkAvatars();
-// }
-// }.runTaskTimerAsynchronously(plugin, 100, 100);
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/randomavatar/util/EndReason.java b/src/main/java/com/projectkorra/rpg/modules/randomavatar/util/EndReason.java
new file mode 100644
index 0000000..76e1092
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/randomavatar/util/EndReason.java
@@ -0,0 +1,6 @@
+package com.projectkorra.rpg.modules.randomavatar.util;
+
+public enum EndReason {
+ DEATH,
+ TIME_RAN_OUT
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEvent.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEvent.java
deleted file mode 100644
index 13722c2..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEvent.java
+++ /dev/null
@@ -1,301 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents;
-
-import com.projectkorra.rpg.ProjectKorraRPG;
-import com.projectkorra.rpg.modules.worldevents.event.WorldEventStartEvent;
-import com.projectkorra.rpg.modules.worldevents.event.WorldEventStopEvent;
-import com.projectkorra.rpg.modules.worldevents.util.DisplayHelper;
-import com.projectkorra.rpg.modules.worldevents.util.display.IWorldEventDisplay;
-import com.projectkorra.rpg.modules.worldevents.util.display.bossbar.BossBarDisplay;
-import com.projectkorra.rpg.modules.worldevents.util.display.bossbar.WorldEventBossBar;
-import com.projectkorra.rpg.modules.worldevents.util.display.chat.ChatDisplay;
-import com.projectkorra.rpg.modules.worldevents.util.display.none.NoDisplay;
-import com.projectkorra.rpg.modules.worldevents.util.display.scoreboard.ScoreboardDisplay;
-import org.bukkit.Bukkit;
-import org.bukkit.NamespacedKey;
-import org.bukkit.Sound;
-import org.bukkit.World;
-import org.bukkit.boss.BarColor;
-import org.bukkit.boss.BarStyle;
-import org.bukkit.configuration.file.FileConfiguration;
-import org.bukkit.configuration.file.YamlConfiguration;
-import org.bukkit.entity.Player;
-import org.bukkit.scheduler.BukkitRunnable;
-
-import java.io.File;
-import java.util.*;
-
-public class WorldEvent {
- private static HashMap ALL_EVENTS = new HashMap<>();
- private static HashSet ACTIVE_EVENTS = new HashSet<>();
- private static HashSet AFFECTED_PLAYERS = new HashSet<>();
-
- private final NamespacedKey worldEventNamespacedKey;
-
- private final String key;
- private String title;
- private long duration;
- private World world;
-
- private List displayMethods;
- private List disabledWorlds;
-
- private final FileConfiguration config;
-
- private WorldEventBossBar worldEventBossBar;
-
- public WorldEvent(String key, String title, long duration, List disabledWorlds, FileConfiguration config, World world, List displayMethods) {
- this.key = key;
- this.title = title;
- this.duration = duration;
- this.disabledWorlds = disabledWorlds;
- this.config = config;
- this.world = world;
- this.displayMethods = (displayMethods == null || displayMethods.isEmpty()) ? Collections.singletonList(new NoDisplay()) : new ArrayList<>(displayMethods);
-
- this.worldEventNamespacedKey = new NamespacedKey(ProjectKorraRPG.getPlugin(), key);
- }
-
- public void startEvent() {
- if (getDisabledWorlds().contains(world)) {
- ProjectKorraRPG.getPlugin().getLogger().info("Couldn't start worldevent because world is a disabled world!");
- return;
- }
-
- getActiveEvents().add(this);
-
- Bukkit.getPluginManager().callEvent(new WorldEventStartEvent(this));
-
- // Add all online players in the world to the Set
- // And play Sound if user configured
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (player.getWorld() == this.world) {
- getAffectedPlayers().add(player);
- if (getConfig().getBoolean("PlayEventStartSound")) {
- String soundName = getConfig().getString("EventStart.Sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
-
- Sound eventStartSound = Sound.valueOf(soundName);
- float volume = (float) getConfig().getDouble("EventStart.Volume");
- float pitch = (float) getConfig().getDouble("EventStart.Pitch");
-
- player.getWorld().playSound(player.getLocation(), eventStartSound, volume, pitch);
- }
- }
- }
-
- // Start the display for the event
- for (IWorldEventDisplay display : getDisplayMethods()) {
- display.startDisplay(this);
- }
-
- startWorldEventTimer();
- }
-
- /**
- * Stop active WorldEvent
- */
- public void stopEvent() {
- Bukkit.getPluginManager().callEvent(new WorldEventStopEvent(this));
- getActiveEvents().remove(this);
-
- // Play EventStop sound for each player in an active WorldEvent world
- for (Player player : getWorld().getPlayers()) {
- if (getAffectedPlayers().contains(player)) {
- if (getConfig().getBoolean("PlayEventStopSound")) {
- String soundName = getConfig().getString("EventStop.Sound", "ENTITY_EXPERIENCE_ORB_PICKUP");
-
- Sound eventStopSound = Sound.valueOf(soundName.toUpperCase());
- float volume = (float) getConfig().getDouble("EventStop.Volume", 1.0);
- float pitch = (float) getConfig().getDouble("EventStop.Pitch", 1.0);
-
- player.getWorld().playSound(player.getLocation(), eventStopSound, volume, pitch);
- }
- }
- }
-
- // Stop the display for the event
- for (IWorldEventDisplay display : getDisplayMethods()) {
- display.stopDisplay(this);
- }
- }
-
- // Updated WorldEvent display
- public void updateDisplay(double progress) {
- for (IWorldEventDisplay display : getDisplayMethods()) {
- display.updateDisplay(this, progress);
- }
- }
-
- /**
- * Puts all WorldEvents from WorldEvents directory into the {@link WorldEvent#getAllEvents()} map
- */
- public static void initAllWorldEvents() {
- File worldEventsFolder = new File(ProjectKorraRPG.getPlugin().getDataFolder(), "WorldEvents");
- if (!worldEventsFolder.exists() || !worldEventsFolder.isDirectory()) {
- ProjectKorraRPG.getPlugin().getLogger().warning("WorldEvents folder was not found!");
- return;
- }
-
- File[] worldEventsFiles = worldEventsFolder.listFiles(((dir, name) -> name.endsWith(".yml")));
- if (worldEventsFiles == null || worldEventsFiles.length == 0) {
- ProjectKorraRPG.getPlugin().getLogger().info("No WorldEvents were found.");
- return;
- }
-
- // Iterate through all WorldEvent configurations
- Arrays.stream(worldEventsFiles).parallel().forEach(file -> {
- String eventKey = file.getName().toLowerCase().replace(".yml", ""); // Event key is file name without yml extension
- FileConfiguration config = YamlConfiguration.loadConfiguration(file);
-
- String eventTitle = config.getString("Title", "&cConfig Title not defined!");
- long duration = config.getLong("Duration", 1000);
-
- String configWorldName = config.getString("World", null);
- World world = (configWorldName == null ? null : Bukkit.getWorld(configWorldName));
-
- List displayMethods = new ArrayList<>();
-
- // BossBar-Display
- if (config.getBoolean("DisplayMethods.BossBar.Enabled", false)) {
- BarColor bossBarColor = DisplayHelper.convertStringToBarColor(config.getString("DisplayMethods.BossBar.Color", "RED"));
- BarStyle bossBarStyle = DisplayHelper.convertStringToBarStyle(config.getString("DisplayMethods.BossBar.Style", "SOLID"));
- boolean smoothBossBar = config.getBoolean("DisplayMethods.BossBar.Smooth", true);
-
- displayMethods.add(new BossBarDisplay(eventTitle, bossBarColor, bossBarStyle, smoothBossBar));
- }
-
- // Chat-Display
- if (config.getBoolean("DisplayMethods.Chat.Enabled", false)) {
- String eventStartMessage = config.getString("DisplayMethods.Chat.EventStartMessage", "&cEventStartMessage not defined!");
- String eventStopMessage = config.getString("DisplayMethods.Chat.EventStopMessage", "&cEventStopMessage not defined!");
-
- displayMethods.add(new ChatDisplay(eventStartMessage, eventStopMessage));
- }
-
- // Scoreboard - Display
- if (config.getBoolean("DisplayMethods.Scoreboard.Enabled", false)) {
- displayMethods.add(new ScoreboardDisplay());
- }
-
- // Parse Disabled Worlds
- List disabledWorldsStringList = config.getStringList("DisabledWorlds");
- List disabledWorlds = new ArrayList<>();
- if (!disabledWorldsStringList.isEmpty()) {
- for (String worldName : disabledWorldsStringList) {
- World w = Bukkit.getWorld(worldName);
- if (w != null) {
- disabledWorlds.add(w);
- }
- }
- }
-
- getAllEvents().put(eventKey, new WorldEvent(eventKey, eventTitle, duration, disabledWorlds, config, world, displayMethods));
- });
- }
-
- private void startWorldEventTimer() {
- final long duration = getDuration();
- final long startTime = System.currentTimeMillis();
-
- new BukkitRunnable() {
- @Override
- public void run() {
- long now = System.currentTimeMillis();
- double elapsed = now - startTime;
- double progress = 1.0 - (elapsed / (double) duration);
-
- if (progress <= 0.0) {
- updateDisplay(0.0);
- stopEvent();
- this.cancel();
- return;
- }
-
- updateDisplay(progress);
- }
- }.runTaskTimer(ProjectKorraRPG.getPlugin(), 0, getWorldEventBossBar().isSmooth() ? 1 : 20);
- }
-
- public static HashMap getAllEvents() {
- return ALL_EVENTS;
- }
-
- public static HashSet getActiveEvents() {
- return ACTIVE_EVENTS;
- }
-
- public static HashSet getAffectedPlayers() {
- return AFFECTED_PLAYERS;
- }
-
- public NamespacedKey getWorldEventNamespacedKey() {
- return worldEventNamespacedKey;
- }
-
- public String getKey() {
- return key;
- }
-
- public String getTitle() {
- return title;
- }
-
- public long getDuration() {
- return duration;
- }
-
- public World getWorld() {
- return world;
- }
-
- public List getDisplayMethods() {
- return displayMethods;
- }
-
- public List getDisabledWorlds() {
- return disabledWorlds;
- }
-
- public FileConfiguration getConfig() {
- return config;
- }
-
- public WorldEventBossBar getWorldEventBossBar() {
- return worldEventBossBar;
- }
-
- public static void setAllEvents(HashMap allEvents) {
- ALL_EVENTS = allEvents;
- }
-
- public static void setActiveEvents(HashSet activeEvents) {
- ACTIVE_EVENTS = activeEvents;
- }
-
- public static void setAffectedPlayers(HashSet affectedPlayers) {
- AFFECTED_PLAYERS = affectedPlayers;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public void setDuration(long duration) {
- this.duration = duration;
- }
-
- public void setWorld(World world) {
- this.world = world;
- }
-
- public void setDisplayMethods(List displayMethods) {
- this.displayMethods = displayMethods;
- }
-
- public void setDisabledWorlds(List disabledWorlds) {
- this.disabledWorlds = disabledWorlds;
- }
-
- public void setWorldEventBossBar(WorldEventBossBar worldEventBossBar) {
- this.worldEventBossBar = worldEventBossBar;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEventModule.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEventModule.java
index 5a38103..614b99c 100644
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEventModule.java
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/WorldEventModule.java
@@ -1,75 +1,81 @@
package com.projectkorra.rpg.modules.worldevents;
+import com.projectkorra.rpg.ProjectKorraRPG;
import com.projectkorra.rpg.modules.Module;
import com.projectkorra.rpg.modules.worldevents.commands.WorldEventCommand;
-import com.projectkorra.rpg.modules.worldevents.listeners.WorldEventModificationListener;
-import com.projectkorra.rpg.modules.worldevents.listeners.WorldEventScheduleListener;
-import com.projectkorra.rpg.modules.worldevents.methods.WorldEventModificationService;
-import com.projectkorra.rpg.modules.worldevents.schedule.WorldEventScheduler;
-import com.projectkorra.rpg.modules.worldevents.schedule.storage.ScheduleStorage;
+import com.projectkorra.rpg.modules.worldevents.factory.WorldEventLoader;
+import com.projectkorra.rpg.modules.worldevents.listener.HandleWorldEventDisplayListener;
+import com.projectkorra.rpg.modules.worldevents.listener.WorldEventModificationListener;
+import com.projectkorra.rpg.modules.worldevents.service.WorldEventModificationService;
+import com.projectkorra.rpg.modules.worldevents.service.WorldEventService;
+import com.projectkorra.rpg.modules.worldevents.storage.ActiveWorldEventIndex;
+import com.projectkorra.rpg.modules.worldevents.storage.WorldEventRegistry;
+import com.projectkorra.rpg.modules.worldevents.util.BossBarCleanup;
import org.bukkit.event.HandlerList;
-import java.util.ArrayList;
-
-public class WorldEventModule extends Module {
+public final class WorldEventModule extends Module {
+ private WorldEventService worldEventService;
+ private WorldEventRegistry worldEventRegistry;
+ private ActiveWorldEventIndex activeWorldEventIndex;
private WorldEventModificationListener modificationListener;
- private WorldEventModificationService modificationService;
-
- private WorldEventScheduleListener scheduleListener;
- private WorldEventScheduler worldEventScheduler;
- private ScheduleStorage scheduleStorage;
+ private HandleWorldEventDisplayListener handleWorldEventDisplayListener;
- public WorldEventModule() {
- super("WorldEvents");
+ public WorldEventModule(ProjectKorraRPG plugin) {
+ super(plugin, "WorldEvents");
}
@Override
public void enable() {
- getPlugin().getLogger().info("Enabling WorldEvent module...");
+ this.getPlugin().getLogger().info("Enabling WorldEvent module...");
- // Initialize all valid WorldEvents found in each config file in the WorldEvents directory
- WorldEvent.initAllWorldEvents();
+ // Store all stale WorldEvents (Not active ones)
+ this.worldEventRegistry = new WorldEventRegistry();
- // Create ModificationService for Listener
- this.modificationService = new WorldEventModificationService();
+ // Register / Store all valid WorldEvents from configurations
+ this.worldEventRegistry.registerAll(new WorldEventLoader(getPlugin()).loadEventsFromFolder().values());
- // Contains necessary methods for DB data retrieval
- this.scheduleStorage = new ScheduleStorage();
+ // Handles Active World Event instances
+ this.activeWorldEventIndex = new ActiveWorldEventIndex();
- // Scheduler to make events start based on config
- this.worldEventScheduler = new WorldEventScheduler(this.scheduleListener, this.scheduleStorage);
+ // Handle business logic of active world events and ticks them
+ this.worldEventService = new WorldEventService(this.getPlugin(), this.activeWorldEventIndex);
- // Create and Register Modification Listener
- this.modificationListener = new WorldEventModificationListener(this.modificationService);
- this.scheduleListener = new WorldEventScheduleListener(this.worldEventScheduler);
-
- // Register Commands
- new WorldEventCommand();
+ // Create Listeners
+ this.modificationListener = new WorldEventModificationListener(new WorldEventModificationService(this.activeWorldEventIndex));
+ this.handleWorldEventDisplayListener = new HandleWorldEventDisplayListener(this.worldEventService);
+ // Register Listeners
registerListeners(
this.modificationListener,
- this.scheduleListener
+ this.handleWorldEventDisplayListener
);
- getPlugin().getLogger().info("WorldEvent module enabled successfully!");
+ // Register Commands
+ new WorldEventCommand(this.worldEventService, this.worldEventRegistry);
+
+ this.getPlugin().getLogger().info("WorldEvent module enabled successfully!");
}
@Override
public void disable() {
- getPlugin().getLogger().info("Disabling WorldEvent module...");
+ this.getPlugin().getLogger().info("Disabling WorldEvent module...");
- // Cleanup Scheduler
- if (this.worldEventScheduler != null) {
- this.worldEventScheduler.cleanup();
- this.worldEventScheduler = null;
- }
+ // Shutdown Service
+ if (this.worldEventService != null) {
+ this.worldEventService.shutdown();
+ this.worldEventService = null;
+ }
- // Stop all active events
- try {
- new ArrayList<>(WorldEvent.getActiveEvents()).forEach(WorldEvent::stopEvent);
- } catch (Exception e) {
- getPlugin().getLogger().severe("Failed to stop all active events!" + e.getMessage());
- }
+ // Nullify Registry
+ if (this.worldEventRegistry != null) {
+ this.worldEventRegistry = null;
+ }
+
+ // Cleanup Active Events Index
+ if (this.activeWorldEventIndex != null) {
+ this.activeWorldEventIndex.clearAll();
+ this.activeWorldEventIndex = null;
+ }
// Unregister ModificationListener
if (this.modificationListener != null) {
@@ -77,27 +83,35 @@ public void disable() {
this.modificationListener = null;
}
- // Clear Worldevent maps
- WorldEvent.getActiveEvents().clear();
- WorldEvent.getAllEvents().clear();
- WorldEvent.getAffectedPlayers().clear();
+ // Unregister EventDisplayListener
+ if (this.handleWorldEventDisplayListener != null) {
+ HandlerList.unregisterAll(this.handleWorldEventDisplayListener);
+ this.handleWorldEventDisplayListener = null;
+ }
- getPlugin().getLogger().info("WorldEvent module disabled successfully!");
- }
+ // Remove Stale / Dead BossBars
+ BossBarCleanup.removeAllFor(this.getPlugin());
- public WorldEventModificationListener getModificationListener() {
- return modificationListener;
+ this.getPlugin().getLogger().info(getName() + " module disabled successfully!");
}
- public WorldEventModificationService getModificationService() {
- return modificationService;
- }
+ public WorldEventService getWorldEventService() {
+ return worldEventService;
+ }
- public WorldEventScheduler getWorldEventScheduler() {
- return this.worldEventScheduler;
- }
+ public WorldEventRegistry getWorldEventRegistry() {
+ return worldEventRegistry;
+ }
- public ScheduleStorage getScheduleStorage() {
- return this.scheduleStorage;
+ public ActiveWorldEventIndex getActiveWorldEventIndex() {
+ return activeWorldEventIndex;
+ }
+
+ public WorldEventModificationListener getModificationListener() {
+ return modificationListener;
}
+
+ public HandleWorldEventDisplayListener getPlayerSwitchWorldListener() {
+ return handleWorldEventDisplayListener;
+ }
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/commands/WorldEventCommand.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/commands/WorldEventCommand.java
index f7d0f24..04d2088 100644
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/commands/WorldEventCommand.java
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/commands/WorldEventCommand.java
@@ -1,70 +1,181 @@
package com.projectkorra.rpg.modules.worldevents.commands;
import com.projectkorra.rpg.commands.RPGCommand;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.gui.WorldEventAttributionGui;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.service.WorldEventService;
+import com.projectkorra.rpg.modules.worldevents.storage.ActiveWorldEventIndex;
+import com.projectkorra.rpg.modules.worldevents.storage.WorldEventRegistry;
+import com.projectkorra.rpg.util.ChatUtil;
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
public class WorldEventCommand extends RPGCommand {
- public WorldEventCommand() {
- super("event", "/bending rpg event start ", "Starts a world event", new String[]{"event", "e", "ev"});
- }
+ private final WorldEventService service;
+ private final WorldEventRegistry registry;
+ private final ActiveWorldEventIndex activeEventsIndex;
+
+ public WorldEventCommand(final WorldEventService service, final WorldEventRegistry registry) {
+ super("event", "/bending rpg event start | /bending rpg event stop ", "Manage worldevents", new String[]{"event", "e", "ev"});
+ this.service = service;
+ this.registry = registry;
+ this.activeEventsIndex = service.getActiveEventsIndex();
+ }
@Override
public void execute(CommandSender sender, List args) {
- if (!(sender instanceof Player player)) {
- sender.sendMessage("Console may not execute this type of command!");
- return;
- }
-
- if (args.size() < 2) {
- help(sender, true);
- return;
- }
-
- if (args.get(0).equalsIgnoreCase("start")) {
- WorldEvent we = WorldEvent.getAllEvents().get(args.get(1).toLowerCase());
-
- if (we == null) {
- sender.sendMessage("WorldEvent '" + args.get(1) + "' not found.");
- } else {
- we.startEvent();
- }
-
- } else if (args.get(0).equalsIgnoreCase("stop")) {
- if (args.get(1) != null) {
- WorldEvent we = WorldEvent.getAllEvents().get(args.get(1).toLowerCase());
-
- if (we == null) {
- sender.sendMessage("WorldEvent '" + args.get(1) + "' not found.");
- } else {
- we.stopEvent();
- }
-
- } else {
- for (WorldEvent worldEvent : WorldEvent.getActiveEvents()) {
- if (worldEvent.getWorld() == player.getWorld()) {
- worldEvent.stopEvent();
- }
- }
- }
- } else {
- help(sender, true);
- }
- }
+ if (args.isEmpty()) {
+ help(sender, true);
+ return;
+ }
- @Override
- protected List getTabCompletion(CommandSender sender, List args) {
- if (args.isEmpty()) {
- return Arrays.asList("start", "stop");
- }
- if (args.size() == 1 && args.getFirst().equalsIgnoreCase("start") || args.size() == 1 && args.getFirst().equalsIgnoreCase("stop")) {
- return WorldEvent.getAllEvents().keySet().stream().sorted().toList();
- }
- return Collections.emptyList();
- }
+ final String sub = args.getFirst().toLowerCase(Locale.ROOT);
+
+ switch (sub) {
+ case "start" -> {
+ if (args.size() != 2) {
+ help(sender, false);
+ return;
+ }
+
+ final String idPath = args.get(1);
+ final WorldEvent worldEvent = registry.findByPath(idPath).orElse(null);
+ if (worldEvent == null) {
+ ChatUtil.sendBrandingMessage(sender, "&cWorldEvent '" + idPath + "' not found.");
+ return;
+ }
+ if (activeEventsIndex.activeWorldEvents().contains(worldEvent)) {
+ ChatUtil.sendBrandingMessage(sender, "&cWorldEvent '" + idPath + "' is already active!");
+ return;
+ }
+
+ // START EVENT
+ boolean started;
+ if (sender instanceof Player player) {
+ World world = player.getWorld();
+ started = service.start(worldEvent, world);
+ } else {
+ started = service.start(worldEvent);
+ }
+
+ if (started) {
+ ChatUtil.sendBrandingMessage(sender, "&aStarted WorldEvent '" + worldEvent.getKey().getKey() + "'.");
+ } else {
+ ChatUtil.sendBrandingMessage(sender, "&cCould not start WorldEvent '" + idPath + "'. Check logs for details.");
+ }
+ }
+
+ case "stop" -> {
+ // STOP ALL
+ if (args.size() == 1) {
+ if (activeEventsIndex.activeWorldEvents().isEmpty()) {
+ ChatUtil.sendBrandingMessage(sender, "&cNo active WorldEvents to stop.");
+ return;
+ }
+ service.stopAll();
+ ChatUtil.sendBrandingMessage(sender, "&aStopped all active WorldEvents.");
+ return;
+ }
+
+ // STOP SPECIFIC
+ if (args.size() == 2) {
+ final String idPath = args.get(1);
+ final WorldEvent we = registry.findByPath(idPath).orElse(null);
+ if (we == null) {
+ ChatUtil.sendBrandingMessage(sender, "&cWorldEvent '" + idPath + "' not found.");
+ return;
+ }
+ if (!activeEventsIndex.activeWorldEvents().contains(we)) {
+ ChatUtil.sendBrandingMessage(sender, "&cWorldEvent '" + idPath + "' is not active.");
+ return;
+ }
+ if (service.stop(we)) {
+ ChatUtil.sendBrandingMessage(sender, "&aStopped world event '" + we.getKey().getKey() + "'.");
+ } else {
+ ChatUtil.sendBrandingMessage(sender, "&cCould not stop world event '" + idPath + "'. Check logs for details.");
+ }
+ } else {
+ help(sender, false);
+ }
+ }
+
+ case "edit" -> {
+ if (args.size() == 1) {
+ ChatUtil.sendBrandingMessage(sender, "&cSpecify WorldEvent to edit!");
+ return;
+ }
+
+ if (args.size() == 2) {
+ final String idPath = args.get(1);
+ final WorldEvent worldEvent = registry.findByPath(idPath).orElse(null);
+ if (worldEvent == null) {
+ ChatUtil.sendBrandingMessage(sender, "&cWorldEvent '" + idPath + "' not found.");
+ return;
+ }
+ if (!isPlayer(sender)) {
+ ChatUtil.sendBrandingMessage(sender, "&cOnly players can edit WorldEvents in game. Console has to do via. configuration files.");
+ return;
+ }
+
+ new WorldEventAttributionGui(worldEvent).open((Player) sender);
+ }
+ }
+
+ default -> help(sender, true);
+ }
+ }
+
+ @Override
+ protected List getTabCompletion(CommandSender sender, List args) {
+ if (args.isEmpty()) {
+ return List.of("start", "stop", "edit");
+ }
+
+ if (args.size() == 1) {
+ final String first = args.getFirst().toLowerCase(Locale.ROOT);
+ final String partialId = (args.size() > 1 ? args.get(1) : "").toLowerCase(Locale.ROOT);
+
+ switch (first) {
+ case "start" -> {
+ return getAllWorldEvents(partialId, true);
+ }
+
+ case "edit" -> {
+ return getAllWorldEvents(partialId, false);
+ }
+
+ case "stop" -> {
+ return activeEventsIndex.activeWorldEvents().stream().parallel()
+ .map(WorldEvent::getKey)
+ .map(NamespacedKey::getKey)
+ .filter(id -> id.toLowerCase(Locale.ROOT).startsWith(partialId))
+ .sorted(String.CASE_INSENSITIVE_ORDER)
+ .collect(Collectors.toList());
+ }
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ private List getAllWorldEvents(String partialId, boolean filterActive) {
+ final Set active = filterActive ? new HashSet<>(activeEventsIndex.activeWorldEvents()) : Collections.emptySet();
+
+ Stream> stream = registry.getAll().entrySet().stream();
+
+ if (filterActive) {
+ stream = stream.filter(e -> !active.contains(e.getValue()));
+ }
+
+ return stream.map(e -> e.getKey().getKey())
+ .filter(id -> id.toLowerCase(Locale.ROOT).startsWith(partialId))
+ .sorted(String.CASE_INSENSITIVE_ORDER)
+ .collect(Collectors.toList());
+ }
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/BossBarDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/BossBarDisplay.java
new file mode 100644
index 0000000..a1a3d9a
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/BossBarDisplay.java
@@ -0,0 +1,103 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import com.projectkorra.projectkorra.util.ChatUtil;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.boss.BarColor;
+import org.bukkit.boss.BarStyle;
+import org.bukkit.boss.KeyedBossBar;
+import org.bukkit.entity.Player;
+
+import java.util.Collection;
+
+public class BossBarDisplay implements IBossBarDisplay {
+ private final NamespacedKey key;
+ private final String title;
+ private final BarColor barColor;
+ private final BarStyle barStyle;
+ private final boolean smooth;
+
+ private KeyedBossBar bossBar;
+
+ private double lastProgress = -1.0;
+ private static final double PROGRESS_EPSILON = 0.01;
+
+ /**
+ * @param barColor Color of BossBar
+ * @param barStyle Style of BossBar
+ * @param smooth true Refresh every tick else every second
+ */
+ public BossBarDisplay(NamespacedKey key, String title, BarColor barColor, BarStyle barStyle, boolean smooth) {
+ this.key = key;
+ this.title = title;
+ this.barColor = barColor;
+ this.barStyle = barStyle;
+ this.smooth = smooth;
+ }
+
+ @Override
+ public void start(WorldEvent event) {
+ KeyedBossBar existing = Bukkit.getBossBar(key);
+ this.bossBar = (existing != null) ? existing : Bukkit.createBossBar(key, ChatUtil.color(title), barColor, barStyle);
+
+ bossBar.setTitle(ChatUtil.color(title));
+ bossBar.setColor(barColor);
+ bossBar.setStyle(barStyle);
+ bossBar.setProgress(1.0);
+ bossBar.setVisible(true);
+ bossBar.removeAll(); // WorldEventService handles viewers
+
+ lastProgress = 1.0;
+ }
+
+ @Override
+ public void stop(WorldEvent event) {
+ if (bossBar == null) return;
+
+ bossBar.removeAll();
+ Bukkit.removeBossBar(key);
+ bossBar = null;
+ lastProgress = -1.0;
+ }
+
+ @Override
+ public long tickPeriod() {
+ return smooth ? 1L : 20L;
+ }
+
+ @Override
+ public void updateTick(WorldEvent event, double progress) {
+ if (bossBar == null) return;
+
+ double clamped = (progress < 0.0) ? 0.0 : (Math.min(progress, 1.0));
+ if (Math.abs(clamped - lastProgress) >= PROGRESS_EPSILON) {
+ bossBar.setProgress(progress);
+ lastProgress = clamped;
+ }
+ }
+
+ @Override
+ public void addViewer(Player viewer) {
+ if (viewer == null || !viewer.isOnline() || bossBar == null) return;
+ bossBar.addPlayer(viewer);
+ }
+
+ @Override
+ public void removeViewer(Player viewer) {
+ if (viewer == null || !viewer.isOnline() || bossBar == null) return;
+ bossBar.removePlayer(viewer);
+ }
+
+ @Override
+ public void addViewers(Collection viewers) {
+ if (viewers == null || viewers.isEmpty() || bossBar == null) return;
+ viewers.forEach(viewer -> bossBar.addPlayer(viewer));
+ }
+
+ @Override
+ public void removeViewers(Collection viewers) {
+ if (viewers == null || viewers.isEmpty() || bossBar == null) return;
+ viewers.forEach(viewer -> bossBar.removePlayer(viewer));
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ChatDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ChatDisplay.java
new file mode 100644
index 0000000..40c0029
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ChatDisplay.java
@@ -0,0 +1,64 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import com.projectkorra.rpg.util.ChatUtil;
+import org.bukkit.entity.Player;
+
+import java.util.Collection;
+
+/**
+ * Chat Display method for an {@link com.projectkorra.rpg.modules.worldevents.models.WorldEvent}
+ * Three types of Messages included:
+ * - EventStart - Event start message
+ * - EventStop - Event stop message
+ * - EventRunning - Event currently active message
+ *
+ * EventRunning is for players joining / re-joining the server
+ * so they will know that an event is currently running (given {@link BossBarDisplay} not active)
+ */
+public class ChatDisplay implements IChatDisplay {
+ private final String startMessage;
+ private final String stopMessage;
+ private final String currentlyActiveMessage;
+
+ public ChatDisplay(String startMessage, String stopMessage, String currentlyActiveMessage) {
+ this.startMessage = startMessage;
+ this.stopMessage = stopMessage;
+ this.currentlyActiveMessage = currentlyActiveMessage;
+ }
+
+ @Override
+ public void sendStartMessage(Player player) {
+ if (startMessage == null || startMessage.isBlank() || player == null || !player.isOnline()) return;
+ ChatUtil.sendBrandingMessage(player, startMessage);
+ }
+
+ @Override
+ public void sendStopMessage(Player player) {
+ if (stopMessage == null || stopMessage.isBlank() || player == null || !player.isOnline()) return;
+ ChatUtil.sendBrandingMessage(player, stopMessage);
+ }
+
+ @Override
+ public void sendEventCurrentlyRunning(Player player) {
+ if (currentlyActiveMessage == null || currentlyActiveMessage.isBlank() || player == null || !player.isOnline()) return;
+ ChatUtil.sendBrandingMessage(player, currentlyActiveMessage);
+ }
+
+ @Override
+ public void sendStartMessage(Collection players) {
+ if (players == null || players.isEmpty()) return;
+ players.forEach(this::sendStartMessage);
+ }
+
+ @Override
+ public void sendStopMessage(Collection players) {
+ if (players == null || players.isEmpty()) return;
+ players.forEach(this::sendStopMessage);
+ }
+
+ @Override
+ public void sendEventCurrentlyRunning(Collection players) {
+ if (players == null || players.isEmpty()) return;
+ players.forEach(this::sendEventCurrentlyRunning);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IBossBarDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IBossBarDisplay.java
new file mode 100644
index 0000000..46d389e
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IBossBarDisplay.java
@@ -0,0 +1,20 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import org.bukkit.entity.Player;
+
+import java.util.Collection;
+
+public interface IBossBarDisplay {
+ void start(WorldEvent event);
+ void stop(WorldEvent event);
+
+ long tickPeriod();
+ void updateTick(WorldEvent event, double progress);
+
+ void addViewer(Player viewer);
+ void removeViewer(Player viewer);
+
+ void addViewers(Collection viewers);
+ void removeViewers(Collection viewers);
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IChatDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IChatDisplay.java
new file mode 100644
index 0000000..ff3589c
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IChatDisplay.java
@@ -0,0 +1,15 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import org.bukkit.entity.Player;
+
+import java.util.Collection;
+
+public interface IChatDisplay {
+ void sendStartMessage(Player player);
+ void sendStopMessage(Player player);
+ void sendEventCurrentlyRunning(Player player);
+
+ void sendStartMessage(Collection players);
+ void sendStopMessage(Collection players);
+ void sendEventCurrentlyRunning(Collection players);
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IScoreBoardDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IScoreBoardDisplay.java
new file mode 100644
index 0000000..d992cd0
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/IScoreBoardDisplay.java
@@ -0,0 +1,3 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+public interface IScoreBoardDisplay {}
\ No newline at end of file
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ISoundDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ISoundDisplay.java
new file mode 100644
index 0000000..671722c
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ISoundDisplay.java
@@ -0,0 +1,13 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import org.bukkit.entity.Player;
+
+import java.util.Collection;
+
+public interface ISoundDisplay {
+ void playStartSound(Player player);
+ void playStopSound(Player player);
+
+ void playStartSound(Collection players);
+ void playStopSound(Collection players);
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ScoreboardDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ScoreboardDisplay.java
new file mode 100644
index 0000000..4755027
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/ScoreboardDisplay.java
@@ -0,0 +1,7 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+/**
+ * TODO: Don't know if we even want a scoreboard display since many users use custom scoreboard plugins or packet based plugins
+ */
+public class ScoreboardDisplay implements IScoreBoardDisplay {
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/display/SoundDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/SoundDisplay.java
new file mode 100644
index 0000000..666a082
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/display/SoundDisplay.java
@@ -0,0 +1,54 @@
+package com.projectkorra.rpg.modules.worldevents.display;
+
+import org.bukkit.Sound;
+import org.bukkit.SoundCategory;
+import org.bukkit.entity.Player;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+
+public class SoundDisplay implements ISoundDisplay {
+ private final @Nullable Sound start;
+ private final float startVolume;
+ private final float startPitch;
+
+ private final @Nullable Sound stop;
+ private final float stopVolume;
+ private final float stopPitch;
+
+ public SoundDisplay(@Nullable Sound start, float startVolume, float startPitch,
+ @Nullable Sound stop, float stopVolume, float stopPitch) {
+ this.start = start;
+ this.startVolume = startVolume;
+ this.startPitch = startPitch;
+
+ this.stop = stop;
+ this.stopVolume = stopVolume;
+ this.stopPitch = stopPitch;
+ }
+
+ @Override
+ public void playStartSound(Player player) {
+ if (player == null || !player.isOnline() || start == null) return;
+ player.playSound(player.getLocation(), start, SoundCategory.AMBIENT, startVolume, startPitch);
+
+ }
+
+ @Override
+ public void playStopSound(Player player) {
+ if (player == null || !player.isOnline() || stop == null) return;
+ player.playSound(player.getLocation(), stop, SoundCategory.AMBIENT, stopVolume, stopPitch);
+ }
+
+ @Override
+ public void playStartSound(Collection players) {
+ if (players == null || players.isEmpty()) return;
+ players.forEach(this::playStartSound);
+ }
+
+ @Override
+ public void playStopSound(Collection players) {
+ if (players == null || players.isEmpty()) return;
+ players.forEach(this::playStopSound);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStartEvent.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStartEvent.java
index 35148c6..6d976df 100644
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStartEvent.java
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStartEvent.java
@@ -1,8 +1,9 @@
package com.projectkorra.rpg.modules.worldevents.event;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
public class WorldEventStartEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@@ -17,7 +18,7 @@ public WorldEvent getWorldEvent() {
}
@Override
- public HandlerList getHandlers() {
+ public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStopEvent.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStopEvent.java
index fae63f7..8965958 100644
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStopEvent.java
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/event/WorldEventStopEvent.java
@@ -1,8 +1,9 @@
package com.projectkorra.rpg.modules.worldevents.event;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
+import org.jetbrains.annotations.NotNull;
public class WorldEventStopEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
@@ -17,7 +18,7 @@ public WorldEvent getWorldEvent() {
}
@Override
- public HandlerList getHandlers() {
+ public @NotNull HandlerList getHandlers() {
return HANDLERS;
}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventBuilder.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventBuilder.java
new file mode 100644
index 0000000..4cf8e67
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventBuilder.java
@@ -0,0 +1,143 @@
+package com.projectkorra.rpg.modules.worldevents.factory;
+
+import com.projectkorra.rpg.modules.worldevents.display.IBossBarDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.IChatDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.ISoundDisplay;
+import com.projectkorra.rpg.modules.worldevents.models.AttributeRules;
+import com.projectkorra.rpg.modules.worldevents.models.ScheduleSpecifications;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+public final class WorldEventBuilder {
+ private NamespacedKey key;
+ private String title;
+ private Long duration;
+
+ private final List scheduledWorlds = new ArrayList<>();
+ private final List disabledWorlds = new ArrayList<>();
+
+ private @Nullable IChatDisplay chatDisplay;
+ private @Nullable IBossBarDisplay bossBarDisplay;
+ private @Nullable ISoundDisplay soundDisplay;
+
+ private @Nullable ScheduleSpecifications schedule;
+ private AttributeRules attributeRules;
+
+ private WorldEventBuilder() {}
+
+ public static WorldEventBuilder create() {
+ return new WorldEventBuilder();
+ }
+
+ public WorldEventBuilder key(final NamespacedKey key) {
+ this.key = key;
+ return this;
+ }
+
+ public WorldEventBuilder title(final String title) {
+ this.title = title;
+ return this;
+ }
+
+ public WorldEventBuilder duration(final long durationMillis) {
+ this.duration = durationMillis;
+ return this;
+ }
+
+ @Deprecated
+ public WorldEventBuilder world(final World world) {
+ if (world != null) this.scheduledWorlds.add(world);
+ return this;
+ }
+
+ public WorldEventBuilder scheduledWorlds(final Collection worlds) {
+ if (worlds != null) {
+ for (World world : worlds) {
+ if (world != null) this.scheduledWorlds.add(world);
+ }
+ }
+ return this;
+ }
+
+ public WorldEventBuilder chatDisplay(final @Nullable IChatDisplay chatDisplay) {
+ if (chatDisplay != null) {
+ this.chatDisplay = chatDisplay;
+ }
+ return this;
+ }
+
+ public WorldEventBuilder bossBarDisplay(final @Nullable IBossBarDisplay bossBarDisplay) {
+ if (bossBarDisplay != null) {
+ this.bossBarDisplay = bossBarDisplay;
+ }
+ return this;
+ }
+
+ public WorldEventBuilder soundDisplay(final @Nullable ISoundDisplay soundDisplay) {
+ if (soundDisplay != null) {
+ this.soundDisplay = soundDisplay;
+ }
+ return this;
+ }
+
+ public WorldEventBuilder disabledWorlds(final Collection worlds) {
+ if (worlds != null) {
+ for (World world : worlds) {
+ if (world != null) this.disabledWorlds.add(world);
+ }
+ }
+ return this;
+ }
+
+ public WorldEventBuilder schedule(final @Nullable ScheduleSpecifications schedule) {
+ if (schedule != null) {
+ this.schedule = schedule;
+ }
+ return this;
+ }
+
+ public WorldEventBuilder attributes(final AttributeRules attributeRules) {
+ this.attributeRules = attributeRules;
+ return this;
+ }
+
+ public Optional tryBuild(Consumer onError) {
+ List errors = new ArrayList<>();
+
+ if (key == null || key.getKey().isBlank()) errors.add("key is missing");
+ if (title == null || title.isBlank()) errors.add("Title is missing / blank");
+ if (duration == null || duration <= 0) errors.add("duration is missing / <= 0");
+
+ // Failed
+ if (!errors.isEmpty()) {
+ if (onError != null) {
+ for (String error : errors) {
+ onError.accept(error);
+ }
+ }
+ return Optional.empty();
+ }
+
+ // Success
+ return Optional.of(new WorldEvent(
+ key,
+ title,
+ duration,
+ List.copyOf(scheduledWorlds),
+ List.copyOf(disabledWorlds),
+ chatDisplay,
+ bossBarDisplay,
+ soundDisplay,
+ schedule,
+ attributeRules
+ ));
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventLoader.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventLoader.java
new file mode 100644
index 0000000..a5108f7
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/factory/WorldEventLoader.java
@@ -0,0 +1,258 @@
+package com.projectkorra.rpg.modules.worldevents.factory;
+
+import com.projectkorra.rpg.ProjectKorraRPG;
+import com.projectkorra.rpg.RPGMethods;
+import com.projectkorra.rpg.modules.worldevents.display.*;
+import com.projectkorra.rpg.modules.worldevents.models.AttributeRules;
+import com.projectkorra.rpg.modules.worldevents.models.ScheduleSpecifications;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.schedule.ScheduleType;
+import com.projectkorra.rpg.modules.worldevents.util.ScheduleParser;
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Sound;
+import org.bukkit.World;
+import org.bukkit.boss.BarColor;
+import org.bukkit.boss.BarStyle;
+import org.bukkit.configuration.ConfigurationSection;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.configuration.file.YamlConfiguration;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+public final class WorldEventLoader {
+ private final ProjectKorraRPG plugin;
+
+ public WorldEventLoader(ProjectKorraRPG plugin) {
+ this.plugin = plugin;
+ }
+
+ public Map loadEventsFromFolder() {
+ Map result = new HashMap<>();
+
+ File directory = new File(plugin.getDataFolder(), "WorldEvents");
+ if (!directory.isDirectory()) {
+ plugin.getLogger().warning("'WorldEvents' folder was not found!");
+ return result;
+ }
+
+ File[] files = directory.listFiles((file, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml"));
+ if (files == null || files.length == 0) {
+ plugin.getLogger().info("'WorldEvents' folder does not contain any WorldEvent files.");
+ return result;
+ }
+
+ int loaded = 0, skipped = 0;
+
+ for (File file : files) {
+ try {
+ FileConfiguration config = YamlConfiguration.loadConfiguration(file);
+
+ // GATHER VALUES FROM CONFIG
+ NamespacedKey key = new NamespacedKey(plugin, file.getName().substring(0, file.getName().length() - 4).toLowerCase(Locale.ROOT));
+ String title = config.getString("Title");
+ long duration = config.getLong("Duration");
+
+ List scheduledWorlds = config.getStringList("Worlds").stream().map(Bukkit::getWorld).filter(Objects::nonNull).collect(Collectors.toList());
+ List disabledWorlds = config.getStringList("DisabledWorlds").stream().map(Bukkit::getWorld).filter(Objects::nonNull).toList();
+
+ IChatDisplay chatDisplay = getChatDisplay(config);
+ IBossBarDisplay bossBarDisplay = getBossBarDisplay(config, key, title);
+ ISoundDisplay soundDisplay = getSoundDisplay(config);
+
+ AttributeRules attributeRules = parseAttributeRules(config);
+ ScheduleSpecifications scheduleSpecifications = parseSchedule(config);
+
+ // CREATE BUILDER
+ WorldEventBuilder builder = WorldEventBuilder.create()
+ .key(key)
+ .title(title)
+ .duration(duration)
+ .scheduledWorlds(scheduledWorlds)
+ .chatDisplay(chatDisplay)
+ .bossBarDisplay(bossBarDisplay)
+ .soundDisplay(soundDisplay)
+ .disabledWorlds(disabledWorlds)
+ .schedule(scheduleSpecifications)
+ .attributes(attributeRules);
+
+ // TRY TO BUILD
+ Optional built = builder.tryBuild(msg -> plugin.getLogger().warning("Skipping '" + file.getName() + "': " + msg));
+ if (built.isPresent()) {
+ // NON-NULL VALUES = Success
+ result.put(key, built.get());
+ loaded++;
+ } else {
+ skipped++;
+ }
+ } catch (Exception exception) {
+ plugin.getLogger().severe("Failed to load " + file.getName() + ": " + exception.getMessage());
+ skipped++;
+ }
+ }
+
+ plugin.getLogger().info("Loaded " + loaded + " WorldEvent(s), skipped " + skipped + ".");
+ return result;
+ }
+
+ private @Nullable IChatDisplay getChatDisplay(FileConfiguration config) {
+ if (!config.isConfigurationSection("DisplayMethods.Chat")) return null;
+ if (!config.getBoolean("DisplayMethods.Chat.Enabled")) return null;
+
+ String startMsg = config.getString("DisplayMethods.Chat.EventStartMessage");
+ String stopMsg = config.getString("DisplayMethods.Chat.EventStopMessage");
+ String runningMsg = config.getString("DisplayMethods.Chat.EventCurrentlyRunning");
+
+ List missing = new ArrayList<>();
+ if (startMsg == null) {
+ missing.add("DisplayMethods.Chat.EventStartMessage");
+ }
+ if (stopMsg == null) {
+ missing.add("DisplayMethods.Chat.EventStopMessage");
+ }
+ if (runningMsg == null) {
+ missing.add("DisplayMethods.Chat.EventCurrentlyRunning");
+ }
+
+ if (!missing.isEmpty()) {
+ plugin.getLogger().warning("Chat display enabled but following entries are missing/blank:");
+ missing.forEach(name -> plugin.getLogger().warning(" - " + name));
+ return null;
+ }
+
+ return new ChatDisplay(startMsg, stopMsg, runningMsg);
+ }
+
+ private @Nullable IBossBarDisplay getBossBarDisplay(FileConfiguration config, NamespacedKey key, String title) {
+ if (!config.isConfigurationSection("DisplayMethods.BossBar")) return null;
+ if (!config.getBoolean("DisplayMethods.BossBar.Enabled")) return null;
+
+ String colorRaw = config.getString("DisplayMethods.BossBar.Color");
+ String styleRaw = config.getString("DisplayMethods.BossBar.Style");
+
+ List missing = new ArrayList<>();
+ if (colorRaw == null || colorRaw.isBlank()) {
+ missing.add("DisplayMethods.BossBar.Color");
+ }
+ if (styleRaw == null || styleRaw.isBlank()) {
+ missing.add("DisplayMethods.BossBar.Style");
+ }
+
+ if (!missing.isEmpty()) {
+ plugin.getLogger().warning("BossBar display enabled but following entries are missing/blank:");
+ missing.forEach(name -> plugin.getLogger().warning(" - " + name));
+ return null;
+ }
+
+ BarColor color = RPGMethods.convertStringToBarColor(colorRaw);
+ BarStyle style = RPGMethods.convertStringToBarStyle(styleRaw);
+ boolean smooth = config.getBoolean("DisplayMethods.BossBar.Smooth", true);
+
+ return new BossBarDisplay(key, title, color, style, smooth);
+ }
+
+ private @Nullable ISoundDisplay getSoundDisplay(FileConfiguration config) {
+ Sound startSound = null;
+ float startVolume = 1F;
+ float startPitch = 1F;
+
+ if (config.getBoolean("PlayEventStartSound")) {
+ String soundId = config.getString("EventStart.Sound");
+ if (soundId != null && !soundId.isBlank()) {
+ startSound = RPGMethods.resolveSound(soundId);
+ startVolume = (float) config.getDouble("EventStart.Volume");
+ startPitch = (float) config.getDouble("EventStart.Pitch");
+ }
+ }
+
+ Sound stopSound = null;
+ float stopVolume = 1F;
+ float stopPitch = 1F;
+
+ if (config.getBoolean("PlayEventStopSound")) {
+ String soundId = config.getString("EventStop.Sound");
+ if (soundId != null && !soundId.isBlank()) {
+ stopSound = RPGMethods.resolveSound(soundId);
+ stopVolume = (float) config.getDouble("EventStop.Volume");
+ stopPitch = (float) config.getDouble("EventStop.Pitch");
+ }
+ }
+
+ if (startSound == null && stopSound == null) return null;
+ return new SoundDisplay(startSound, startVolume, startPitch, stopSound, stopVolume, stopPitch);
+ }
+
+ private AttributeRules parseAttributeRules(FileConfiguration config) {
+ ConfigurationSection root = config.getConfigurationSection("Abilities");
+ if (root == null) {
+ return new AttributeRules(Map.of(), Map.of(), Map.of());
+ }
+
+ Map global = Map.of();
+ ConfigurationSection globalSec = root.getConfigurationSection("_All");
+ if (globalSec != null) {
+ global = globalSec.getValues(false);
+ }
+
+ Map> byElement = new HashMap<>();
+ Map>> byAbility = new HashMap<>();
+
+ for (String elemKey : root.getKeys(false)) {
+ if ("_All".equalsIgnoreCase(elemKey)) continue;
+
+ ConfigurationSection elemSec = root.getConfigurationSection(elemKey);
+ if (elemSec == null) continue;
+
+ ConfigurationSection elemAll = elemSec.getConfigurationSection("_All");
+ if (elemAll != null) {
+ byElement.put(elemKey, elemAll.getValues(false));
+ }
+
+ Map> abilityMap = new HashMap<>();
+ for (String child : elemSec.getKeys(false)) {
+ if ("_All".equalsIgnoreCase(child)) continue;
+ ConfigurationSection abilitySec = elemSec.getConfigurationSection(child);
+ if (abilitySec == null) continue;
+
+ Map attrs = abilitySec.getValues(false);
+ if (!attrs.isEmpty()) {
+ abilityMap.put(child, attrs);
+ }
+ }
+ if (!abilityMap.isEmpty()) {
+ byAbility.put(elemKey, abilityMap);
+ }
+ }
+
+ return new AttributeRules(global, byElement, byAbility);
+ }
+
+ /**
+ * TEMP METHOD
+ */
+ private @Nullable ScheduleSpecifications parseSchedule(FileConfiguration config) {
+ ConfigurationSection sec = config.getConfigurationSection("Schedule");
+ if (sec == null) {
+ return null;
+ }
+
+ String rawCalendar = sec.getString("Calendar", "REALTIME");
+ ScheduleType type = ScheduleType.fromString(rawCalendar);
+ ScheduleSpecifications.Calendar calendar = (type == ScheduleType.IN_GAME_DAYS)
+ ? ScheduleSpecifications.Calendar.IN_GAME_DAYS
+ : ScheduleSpecifications.Calendar.REAL_DAYS;
+
+ LocalTime at = ScheduleParser.parseTimeOfDay(sec.getString("At"), LocalTime.of(7, 0));
+ Duration repeat = ScheduleParser.parseDuration(sec.getString("Repeat"), Duration.ofDays(7));
+ Duration offset = ScheduleParser.parseDuration(sec.getString("Offset"), Duration.ZERO);
+ Duration cooldown = ScheduleParser.parseDuration(sec.getString("Cooldown"), Duration.ofDays(1));
+ double chance = sec.getDouble("TriggerChance", 0.5D);
+
+ return new ScheduleSpecifications(calendar, at, repeat, offset, cooldown, chance);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/gui/WorldEventAttributionGui.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/gui/WorldEventAttributionGui.java
new file mode 100644
index 0000000..e5af968
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/gui/WorldEventAttributionGui.java
@@ -0,0 +1,31 @@
+package com.projectkorra.rpg.modules.worldevents.gui;
+
+import com.projectkorra.projectkorra.util.ChatUtil;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.ui.InventoryUI;
+import com.projectkorra.rpg.ui.builder.InventoryUIBuilder;
+import com.projectkorra.rpg.ui.menu.Menu;
+import org.bukkit.Material;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+
+public class WorldEventAttributionGui implements Menu {
+ private static final int ROWS = 3;
+ private final WorldEvent worldEvent;
+
+ public WorldEventAttributionGui(WorldEvent worldEvent) {
+ this.worldEvent = worldEvent;
+ }
+
+ @Override
+ public InventoryUI buildUI(Player player) {
+ return InventoryUIBuilder.create(ROWS, ChatUtil.color(worldEvent.getTitle()))
+ .fill(new ItemStack(Material.GRAY_STAINED_GLASS_PANE))
+ .withItem(0, 0, new ItemStack(Material.BLAZE_POWDER))
+ .build();
+ }
+
+ public WorldEvent getWorldEvent() {
+ return worldEvent;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/HandleWorldEventDisplayListener.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/HandleWorldEventDisplayListener.java
new file mode 100644
index 0000000..d594472
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/HandleWorldEventDisplayListener.java
@@ -0,0 +1,68 @@
+package com.projectkorra.rpg.modules.worldevents.listener;
+
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.service.WorldEventService;
+import com.projectkorra.rpg.modules.worldevents.storage.ActiveWorldEventIndex;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerChangedWorldEvent;
+import org.bukkit.event.player.PlayerJoinEvent;
+import org.bukkit.event.player.PlayerQuitEvent;
+import org.bukkit.event.world.WorldUnloadEvent;
+
+import java.util.ArrayList;
+
+/**
+ * Handle players switching worlds to remove
+ * player BossBar and generally from a {@link WorldEvent}
+ */
+public class HandleWorldEventDisplayListener implements Listener {
+ private final WorldEventService service;
+ private final ActiveWorldEventIndex index;
+
+ public HandleWorldEventDisplayListener(WorldEventService service) {
+ this.service = service;
+ this.index = service.getActiveEventsIndex();
+ }
+
+ @EventHandler
+ public void onWorldSwitch(final PlayerChangedWorldEvent event) {
+ final Player player = event.getPlayer();
+
+ for (WorldEvent worldEvent : new ArrayList<>(index.getActiveIn(event.getFrom()))) {
+ service.removeViewer(worldEvent, player);
+ }
+
+ for (WorldEvent worldEvent : new ArrayList<>(index.getActiveIn(player.getWorld()))) {
+ service.addViewer(worldEvent, player);
+ service.sendWorldEventRunningMessage(worldEvent, player);
+ }
+ }
+
+ @EventHandler
+ public void onJoin(final PlayerJoinEvent event) {
+ for (WorldEvent worldEvent : new ArrayList<>(index.getActiveIn(event.getPlayer().getWorld()))) {
+ service.addViewer(worldEvent, event.getPlayer());
+ service.sendWorldEventRunningMessage(worldEvent, event.getPlayer());
+ }
+ }
+
+ @EventHandler
+ public void onQuit(final PlayerQuitEvent event) {
+ for (WorldEvent worldEvent : new ArrayList<>(index.getActiveIn(event.getPlayer().getWorld()))) {
+ service.removeViewer(worldEvent, event.getPlayer());
+ }
+ }
+
+ @EventHandler
+ public void onWorldUnload(WorldUnloadEvent event) {
+ for (WorldEvent worldEvent : new ArrayList<>(index.getActiveIn(event.getWorld()))) {
+ service.stop(worldEvent);
+ }
+ }
+
+ public WorldEventService getService() {
+ return service;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/WorldEventModificationListener.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/WorldEventModificationListener.java
new file mode 100644
index 0000000..0a6d8ca
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/listener/WorldEventModificationListener.java
@@ -0,0 +1,29 @@
+package com.projectkorra.rpg.modules.worldevents.listener;
+
+import com.projectkorra.projectkorra.BendingPlayer;
+import com.projectkorra.projectkorra.event.AbilityRecalculateAttributeEvent;
+import com.projectkorra.rpg.modules.worldevents.service.WorldEventModificationService;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+
+public class WorldEventModificationListener implements Listener {
+ private final WorldEventModificationService modificationService;
+
+ public WorldEventModificationListener(final WorldEventModificationService modificationService) {
+ this.modificationService = modificationService;
+ }
+
+ @EventHandler(priority = EventPriority.LOW)
+ public void onAttributeRecalc(final AbilityRecalculateAttributeEvent event) {
+ BendingPlayer bendingPlayer = event.getAbility().getBendingPlayer();
+ if (bendingPlayer == null) return;
+ Player player = bendingPlayer.getPlayer();
+ if (player == null || !player.isOnline()) return;
+
+ if (!modificationService.getActiveEventsIndex().hasActiveIn(player.getWorld())) return;
+
+ modificationService.applyWorldEventMods(event, player.getWorld());
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventModificationListener.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventModificationListener.java
deleted file mode 100644
index c2b3f49..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventModificationListener.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.listeners;
-
-import com.projectkorra.projectkorra.ability.CoreAbility;
-import com.projectkorra.projectkorra.event.AbilityRecalculateAttributeEvent;
-import com.projectkorra.rpg.modules.worldevents.event.WorldEventStopEvent;
-import com.projectkorra.rpg.modules.worldevents.methods.WorldEventModificationService;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.EventPriority;
-import org.bukkit.event.Listener;
-
-public class WorldEventModificationListener implements Listener {
- private final WorldEventModificationService modificationService;
-
- public WorldEventModificationListener(WorldEventModificationService modificationService) {
- this.modificationService = modificationService;
- }
-
- @EventHandler(priority = EventPriority.LOW)
- public void onAttributeRecalc(final AbilityRecalculateAttributeEvent event) {
- this.modificationService.applyWorldEventMods(event);
- }
-
- @EventHandler
- public void onWorldEventStop(final WorldEventStopEvent event) {
- for (CoreAbility ability : CoreAbility.getAbilitiesByInstances()) {
- ability.recalculateAttributes();
- }
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventScheduleListener.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventScheduleListener.java
deleted file mode 100644
index 9d13825..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/listeners/WorldEventScheduleListener.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.listeners;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.event.WorldEventStartEvent;
-import com.projectkorra.rpg.modules.worldevents.event.WorldEventStopEvent;
-import com.projectkorra.rpg.modules.worldevents.schedule.WorldEventScheduler;
-import org.bukkit.event.EventHandler;
-import org.bukkit.event.Listener;
-
-public class WorldEventScheduleListener implements Listener {
- private final WorldEventScheduler scheduler;
-
- public WorldEventScheduleListener(WorldEventScheduler scheduler) {
- this.scheduler = scheduler;
- }
-
- @EventHandler
- public void onWorldEventStart(WorldEventStartEvent event) {
- scheduler.setEventActive(event.getWorldEvent(), true);
- }
-
- @EventHandler
- public void onWorldEventStop(WorldEventStopEvent event) {
- WorldEvent worldEvent = event.getWorldEvent();
-
- // Mark the event as inactive
- scheduler.setEventActive(worldEvent, false);
-
- // Reschedule it
- scheduler.rescheduleEvent(worldEvent);
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/methods/WorldEventModificationService.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/methods/WorldEventModificationService.java
deleted file mode 100644
index 2389078..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/methods/WorldEventModificationService.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.methods;
-
-import com.projectkorra.projectkorra.attribute.AttributeModification;
-import com.projectkorra.projectkorra.attribute.AttributeModifier;
-import com.projectkorra.projectkorra.attribute.AttributeUtil;
-import com.projectkorra.projectkorra.event.AbilityRecalculateAttributeEvent;
-import com.projectkorra.rpg.ProjectKorraRPG;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import commonslang3.projectkorra.lang3.tuple.Pair;
-import org.bukkit.NamespacedKey;
-import org.bukkit.configuration.file.FileConfiguration;
-
-/**
- * A service class responsible for applying world-event-based attribute modifications to abilities.
- * It processes active world events and applies relevant attribute modifications configured within
- * the world event's configuration file to the abilities based on their context (element, ability name, attribute name)
- * Has to listen to the {@link AbilityRecalculateAttributeEvent} to function.
- */
-public class WorldEventModificationService {
- private static final String GLOBAL_PATH_FORMAT = "Abilities._All.%s";
- private static final String ELEMENTS_PATH_FORMAT = "Abilities.%s._All.%s";
- private static final String ABILITIES_PATH_FORMAT = "Abilities.%s.%s.%s";
-
- /**
- * Applies modifications from active worldevents to the abilities configured in the coresponding config
- */
- public void applyWorldEventMods(AbilityRecalculateAttributeEvent event) {
- AttributeContext context = new AttributeContext(
- event.getAbility().getElement().getName(),
- event.getAbility().getName(),
- event.getAttribute()
- );
-
- WorldEvent.getActiveEvents().forEach(worldEvent -> processWorldEvent(event, worldEvent, context));
- }
-
- private void processWorldEvent(AbilityRecalculateAttributeEvent event, WorldEvent worldEvent, AttributeContext context) {
- Object rawValue = findConfigurationValue(worldEvent.getConfig(), context);
- if (rawValue == null) return;
-
- AttributeModification mod = buildModification(rawValue, worldEvent.getWorldEventNamespacedKey());
- if (mod != null) {
- event.addModification(mod);
- }
- }
-
- private Object findConfigurationValue(FileConfiguration config, AttributeContext context) {
- // Specific ability path
- String abilitySpecificPath = String.format(ABILITIES_PATH_FORMAT, context.element(), context.abilityName(), context.attributeName());
- Object value = config.get(abilitySpecificPath);
- if (value != null) return value;
-
- // Element path
- String elementPath = String.format(ELEMENTS_PATH_FORMAT, context.element(), context.attributeName());
- value = config.get(elementPath);
- if (value != null) return value;
-
- // Global path
- String globalPath = String.format(GLOBAL_PATH_FORMAT, context.attributeName());
- value = config.get(globalPath);
- return value;
- }
-
- private AttributeModification buildModification(Object raw, NamespacedKey nsKey) {
- if (raw instanceof Boolean) {
- return AttributeModification.setter((Boolean) raw, AttributeModification.PRIORITY_NORMAL, nsKey);
- }
-
- if (raw instanceof Number) {
- return AttributeModification.of(AttributeModifier.SET, (Number) raw, AttributeModification.PRIORITY_NORMAL, nsKey);
- }
-
- String rawStr = raw.toString().replace(" ", "");
- Pair parsed = AttributeUtil.getModification(rawStr);
-
- if (parsed != null) {
- return AttributeModification.of(parsed.getLeft(), parsed.getRight(), AttributeModification.PRIORITY_NORMAL, nsKey);
- }
-
- ProjectKorraRPG.getPlugin().getLogger().warning("WorldEvent parse failed for key=" + nsKey.getKey() + " raw=" + rawStr);
- return null;
- }
-
- record AttributeContext(String element, String abilityName, String attributeName) {}
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ActiveWorldEvent.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ActiveWorldEvent.java
new file mode 100644
index 0000000..a4125e3
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ActiveWorldEvent.java
@@ -0,0 +1,162 @@
+package com.projectkorra.rpg.modules.worldevents.models;
+
+import com.projectkorra.rpg.modules.worldevents.display.IBossBarDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.IChatDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.ISoundDisplay;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+public final class ActiveWorldEvent {
+ private final WorldEvent worldEvent;
+ private final Set viewers = new HashSet<>();
+
+ private final IChatDisplay chatDisplay;
+ private final IBossBarDisplay bossBarDisplay;
+ private final ISoundDisplay soundDisplay;
+
+ private final World runtimeWorld;
+ private final long updateEveryTicks;
+ private final boolean requiresTicking;
+
+ private long startTime;
+ private long nextUpdateAtTickNo = 0L;
+
+ public ActiveWorldEvent(final WorldEvent worldEvent, final World runtimeWorld) {
+ this.worldEvent = worldEvent;
+ this.chatDisplay = worldEvent.getChatDisplay();
+ this.bossBarDisplay = worldEvent.getBossBarDisplay();
+ this.soundDisplay = worldEvent.getSoundDisplay();
+ this.runtimeWorld = runtimeWorld;
+
+ this.requiresTicking = bossBarDisplay != null;
+ this.updateEveryTicks = computePeriod(bossBarDisplay);
+ }
+
+ public void start() {
+ this.startTime = System.currentTimeMillis();
+ this.startDisplays();
+ }
+
+ private void startDisplays() {
+ if (chatDisplay != null) {
+ chatDisplay.sendStartMessage(runtimeWorld.getPlayers());
+ }
+
+ if (bossBarDisplay != null) {
+ bossBarDisplay.start(worldEvent);
+ bossBarDisplay.addViewers(runtimeWorld.getPlayers());
+ }
+
+ if (soundDisplay != null) {
+ soundDisplay.playStartSound(runtimeWorld.getPlayers());
+ }
+ }
+
+ public void stop() {
+ this.stopDisplays();
+ viewers.clear();
+ }
+
+ private void stopDisplays() {
+ if (chatDisplay != null) {
+ chatDisplay.sendStopMessage(runtimeWorld.getPlayers());
+ }
+
+ if (bossBarDisplay != null) {
+ bossBarDisplay.stop(worldEvent);
+ bossBarDisplay.removeViewers(runtimeWorld.getPlayers());
+ }
+
+ if (soundDisplay != null) {
+ soundDisplay.playStopSound(runtimeWorld.getPlayers());
+ }
+ }
+
+ /**
+ * @return true if expired and should be stopped by Service
+ */
+ public boolean tick(long tickNo, long nowMillis) {
+ // Ticking not required?
+ if (!requiresTicking) {
+ return (nowMillis - startTime) >= worldEvent.getDuration();
+ }
+
+ // Throttle updates to configured option (smooth, nonsmooth)
+ if (tickNo < nextUpdateAtTickNo) return false;
+ nextUpdateAtTickNo = tickNo + updateEveryTicks;
+
+ double elapsed = nowMillis - startTime;
+ double progress = 1.0 - (elapsed / (double) worldEvent.getDuration());
+
+ if (progress <= 0.0) {
+ if (bossBarDisplay != null) {
+ bossBarDisplay.updateTick(worldEvent, 0.0);
+ }
+ return true; // Expired
+ }
+
+ double clamped = Math.min(progress, 1.0);
+ if (bossBarDisplay != null) {
+ bossBarDisplay.updateTick(worldEvent, clamped);
+ }
+
+ return false;
+ }
+
+ public void addViewer(Player viewer) {
+ if (viewers.add(viewer.getUniqueId())) {
+ if (bossBarDisplay != null) {
+ bossBarDisplay.addViewer(viewer);
+ }
+ }
+ }
+
+ public void removeViewer(Player viewer) {
+ if (viewers.remove(viewer.getUniqueId())) {
+ if (bossBarDisplay != null) {
+ bossBarDisplay.removeViewer(viewer);
+ }
+ }
+ }
+
+ public void sendWorldEventRunningMessage(Player player) {
+ if (chatDisplay != null) {
+ chatDisplay.sendEventCurrentlyRunning(player);
+ }
+ }
+
+ private static long computePeriod(IBossBarDisplay display) {
+ if (display == null) return 20L;
+
+ long min = Long.MAX_VALUE;
+ long p = display.tickPeriod();
+ if (p < 1L) p = 1L;
+ if (p < min) min = p;
+
+ return (min == Long.MAX_VALUE) ? 20L : min;
+ }
+
+ public boolean requiresTicking() {
+ return requiresTicking;
+ }
+
+ public WorldEvent getWorldEvent() {
+ return worldEvent;
+ }
+
+ public World getRuntimeWorld() {
+ return runtimeWorld;
+ }
+
+ @Override
+ public String toString() {
+ return "ActiveWorldEvent{key=" + worldEvent.getKey() +
+ ", world=" + (runtimeWorld != null ? runtimeWorld.getName() : "null") +
+ ", viewers=" + viewers.size() +
+ ", updateEveryTicks=" + updateEveryTicks + "}";
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/AttributeRules.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/AttributeRules.java
new file mode 100644
index 0000000..4c822f3
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/AttributeRules.java
@@ -0,0 +1,40 @@
+package com.projectkorra.rpg.modules.worldevents.models;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public final class AttributeRules {
+ private final Map global; // attr -> value
+ private final Map> byElement; // element -> (attr -> value)
+ private final Map>> byAbility; // element -> ability -> (attr -> value)
+
+ public AttributeRules(Map global, Map> byElement, Map>> byAbility) {
+ this.global = Map.copyOf(Objects.requireNonNull(global));
+ this.byElement = byElement.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> Map.copyOf(e.getValue())));
+ this.byAbility = byAbility.entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> e.getValue().entrySet().stream()
+ .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, ee -> Map.copyOf(ee.getValue())))));
+ }
+
+ public Object find(String element, String ability, String attribute) {
+ // ability scope
+ Map> byElementMap = byAbility.get(element);
+ if (byElementMap != null) {
+ Map abilityMap = byElementMap.get(ability);
+ if (abilityMap != null) {
+ Object v = abilityMap.get(attribute);
+ if (v != null) return v;
+ }
+ }
+
+ // element scope
+ Map elemAttrs = byElement.get(element);
+ if (elemAttrs != null) {
+ Object v = elemAttrs.get(attribute);
+ if (v != null) return v;
+ }
+
+ // global
+ return global.get(attribute);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ScheduleSpecifications.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ScheduleSpecifications.java
new file mode 100644
index 0000000..6947499
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/ScheduleSpecifications.java
@@ -0,0 +1,51 @@
+package com.projectkorra.rpg.modules.worldevents.models;
+
+import java.time.Duration;
+import java.time.LocalTime;
+
+public final class ScheduleSpecifications {
+ public enum Calendar {
+ REAL_DAYS,
+ IN_GAME_DAYS
+ }
+
+ private final Calendar calendar;
+ private final LocalTime localTime; // tim of day trigger
+ private final Duration repeat; // how often to check / trigger
+ private final Duration offset; // random offset windows
+ private final Duration cooldown; // min time between triggers
+ private final double triggerChance;
+
+ public ScheduleSpecifications(Calendar calendar, LocalTime localTime, Duration repeat, Duration offset, Duration cooldown, double triggerChance) {
+ this.calendar = calendar;
+ this.localTime = localTime;
+ this.repeat = repeat;
+ this.offset = offset;
+ this.cooldown = cooldown;
+ this.triggerChance = triggerChance;
+ }
+
+ public Calendar getCalendar() {
+ return calendar;
+ }
+
+ public LocalTime getLocalTime() {
+ return localTime;
+ }
+
+ public Duration getRepeat() {
+ return repeat;
+ }
+
+ public Duration getOffset() {
+ return offset;
+ }
+
+ public Duration getCooldown() {
+ return cooldown;
+ }
+
+ public double getTriggerChance() {
+ return triggerChance;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/WorldEvent.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/WorldEvent.java
new file mode 100644
index 0000000..d7b4cc9
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/WorldEvent.java
@@ -0,0 +1,132 @@
+package com.projectkorra.rpg.modules.worldevents.models;
+
+import com.projectkorra.rpg.modules.worldevents.display.IBossBarDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.IChatDisplay;
+import com.projectkorra.rpg.modules.worldevents.display.ISoundDisplay;
+import org.bukkit.Keyed;
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public final class WorldEvent implements Keyed {
+ private final NamespacedKey key;
+ private final String title;
+ private final long duration;
+
+ private final List scheduledWorlds;
+ private final List disabledWorlds;
+
+ private final @Nullable IChatDisplay chatDisplay;
+ private final @Nullable IBossBarDisplay bossBarDisplay;
+ private final @Nullable ISoundDisplay soundDisplay;
+
+ private final @Nullable ScheduleSpecifications scheduleSpecifications;
+ private final AttributeRules attributeRules;
+
+ public WorldEvent(
+ NamespacedKey key,
+ String title,
+ long duration,
+ List scheduledWorlds,
+ List disabledWorlds,
+ @Nullable IChatDisplay chatDisplay,
+ @Nullable IBossBarDisplay bossBarDisplay,
+ @Nullable ISoundDisplay soundDisplay,
+ @Nullable ScheduleSpecifications scheduleSpecifications,
+ AttributeRules attributeRules)
+ {
+ this.key = key;
+ this.title = title;
+ this.duration = duration;
+ this.scheduledWorlds = scheduledWorlds == null ? List.of() : java.util.List.copyOf(scheduledWorlds);
+ this.disabledWorlds = disabledWorlds == null ? List.of() : java.util.List.copyOf(disabledWorlds);
+ this.chatDisplay = chatDisplay;
+ this.bossBarDisplay = bossBarDisplay;
+ this.soundDisplay = soundDisplay;
+ this.scheduleSpecifications = scheduleSpecifications;
+ this.attributeRules = attributeRules;
+ }
+
+ @Override
+ public @NotNull NamespacedKey getKey() {
+ return this.key;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public long getDuration() {
+ return duration;
+ }
+
+ public List getScheduledWorlds() {
+ return scheduledWorlds;
+ }
+
+ public List getDisabledWorlds() {
+ return disabledWorlds;
+ }
+
+ public boolean isWorldDisabled(World world) {
+ return world != null && disabledWorlds.contains(world);
+ }
+
+ public @Nullable IChatDisplay getChatDisplay() {
+ return chatDisplay;
+ }
+
+ public @Nullable IBossBarDisplay getBossBarDisplay() {
+ return bossBarDisplay;
+ }
+
+ public @Nullable ISoundDisplay getSoundDisplay() {
+ return soundDisplay;
+ }
+
+ public @Nullable ScheduleSpecifications getScheduleSpecifications() {
+ return scheduleSpecifications;
+ }
+
+ public AttributeRules getAttributeRules() {
+ return attributeRules;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof WorldEvent other)) return false;
+ return key.equals(other.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ String scheduled = scheduledWorlds.isEmpty()
+ ? "[]"
+ : scheduledWorlds.stream()
+ .map(World::getName)
+ .collect(Collectors.joining(", ", "[", "]"));
+ String disabled = disabledWorlds.isEmpty()
+ ? "[]"
+ : disabledWorlds.stream()
+ .map(World::getName)
+ .collect(Collectors.joining(", ", "[", "]"));
+
+ return "WorldEvent{hashcode=" + hashCode() +
+ ", key=" + key +
+ ", title='" + title + '\'' +
+ ", duration=" + duration +
+ ", scheduledWorlds=" + scheduled +
+ ", disabledWorlds=" + disabled +
+ "}";
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/AbilityAttribute.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/AbilityAttribute.java
new file mode 100644
index 0000000..17f786a
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/AbilityAttribute.java
@@ -0,0 +1,4 @@
+package com.projectkorra.rpg.modules.worldevents.models.attributes;
+
+public class AbilityAttribute {
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/ElementAttribute.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/ElementAttribute.java
new file mode 100644
index 0000000..3461119
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/ElementAttribute.java
@@ -0,0 +1,4 @@
+package com.projectkorra.rpg.modules.worldevents.models.attributes;
+
+public class ElementAttribute {
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/GlobalAttribute.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/GlobalAttribute.java
new file mode 100644
index 0000000..93606ef
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/models/attributes/GlobalAttribute.java
@@ -0,0 +1,4 @@
+package com.projectkorra.rpg.modules.worldevents.models.attributes;
+
+public class GlobalAttribute {
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/ScheduleType.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/ScheduleType.java
new file mode 100644
index 0000000..bbf7349
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/ScheduleType.java
@@ -0,0 +1,13 @@
+package com.projectkorra.rpg.modules.worldevents.schedule;
+
+/**
+ * TEMP CLASS, package will be filled with future schedule update
+ */
+public enum ScheduleType {
+ IN_GAME_DAYS,
+ REAL_DAYS;
+
+ public static ScheduleType fromString(String rawCalendar) {
+ return IN_GAME_DAYS;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategy.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategy.java
deleted file mode 100644
index c680935..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategy.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import org.bukkit.plugin.Plugin;
-
-public interface WorldEventScheduleStrategy {
- void scheduleNext(WorldEvent event, Plugin plugin);
- void cancelSchedule();
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategyFactory.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategyFactory.java
deleted file mode 100644
index b1d255e..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduleStrategyFactory.java
+++ /dev/null
@@ -1,109 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule;
-
-import com.projectkorra.rpg.ProjectKorraRPG;
-import com.projectkorra.rpg.modules.worldevents.schedule.storage.ScheduleStorage;
-import com.projectkorra.rpg.modules.worldevents.schedule.strategies.EveryInGameDaysStrategy;
-import com.projectkorra.rpg.modules.worldevents.schedule.strategies.EveryRealWorldDaysStrategy;
-import com.projectkorra.rpg.modules.worldevents.schedule.strategies.util.ScheduleType;
-import org.bukkit.configuration.file.FileConfiguration;
-
-import java.time.Duration;
-import java.time.LocalTime;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class WorldEventScheduleStrategyFactory {
- public static WorldEventScheduleStrategy get(FileConfiguration config, ScheduleStorage scheduleStorage) {
- String rawType = config.getString("Schedule.Calendar", "REALTIME");
- ScheduleType scheduleType = ScheduleType.fromString(rawType);
-
- LocalTime timeOfDay = parseTimeOfDay(config.getString("Schedule.At", "7am"));
- Duration repeatDuration = parseDuration(config.getString("Schedule.Repeat", "7d"));
- Duration offsetDuration = parseDuration(config.getString("Schedule.Offset", "1d5h"));
- Duration cooldown = parseDuration(config.getString("Schedule.Cooldown", "1d"));
- double chance = config.getDouble("Schedule.TriggerChance", 0.5);
-
- return switch (scheduleType) {
- case REAL_DAYS -> new EveryRealWorldDaysStrategy(
- timeOfDay,
- repeatDuration,
- offsetDuration,
- chance,
- cooldown,
- scheduleStorage
- );
-
- case IN_GAME_DAYS -> new EveryInGameDaysStrategy(
- timeOfDay,
- repeatDuration,
- offsetDuration,
- chance,
- cooldown,
- scheduleStorage
- );
- };
- }
-
- /**
- * Parses a human-readable time of day string like "7am", "3:30pm" into a LocalTime
- */
- private static LocalTime parseTimeOfDay(String timeStr) {
- timeStr = timeStr.toLowerCase().trim();
-
- // Match patterns like "7am", "7:30am", "15:45", etc.
- Matcher matcher = Pattern.compile("(\\d{1,2})(?::(\\d{2}))?(am|pm)?").matcher(timeStr);
-
- if (matcher.matches()) {
- int hour = Integer.parseInt(matcher.group(1));
- int minute = matcher.group(2) != null ? Integer.parseInt(matcher.group(2)) : 0;
- String period = matcher.group(3);
-
- // Handle 12-hour clock format
- if (period != null) {
- if (period.equals("pm") && hour < 12) {
- hour += 12;
- } else if (period.equals("am") && hour == 12) {
- hour = 0;
- }
- }
-
- return LocalTime.of(hour, minute);
- }
-
- // Default to 7am if unparseable
- ProjectKorraRPG.getPlugin().getLogger().warning("Could not parse time of day: " + timeStr + ", defaulting to 7am");
- return LocalTime.of(7, 0);
- }
-
- /**
- * Parses a duration string like "7d", "3d12h", "30m2s" into a Duration
- */
- private static Duration parseDuration(String durationStr) {
- durationStr = durationStr.toLowerCase().trim();
-
- long totalSeconds = 0;
-
- // Match patterns like "7d", "12h", "30m", "45s", or combinations like "3d12h30m"
- Matcher matcher = Pattern.compile("(\\d+)([dhms])").matcher(durationStr);
-
- while (matcher.find()) {
- int value = Integer.parseInt(matcher.group(1));
- String unit = matcher.group(2);
-
- switch (unit) {
- case "d" -> totalSeconds += value * 86400L; // days to seconds
- case "h" -> totalSeconds += value * 3600L; // hours to seconds
- case "m" -> totalSeconds += value * 60L; // minutes to seconds
- case "s" -> totalSeconds += value; // seconds
- }
- }
-
- // Default to 1 day if unparseable
- if (totalSeconds == 0) {
- ProjectKorraRPG.getPlugin().getLogger().warning("Could not parse duration: " + durationStr + ", defaulting to 1d");
- totalSeconds = 86400L;
- }
-
- return Duration.ofSeconds(totalSeconds);
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduler.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduler.java
deleted file mode 100644
index dcf81eb..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/WorldEventScheduler.java
+++ /dev/null
@@ -1,175 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule;
-
-import com.projectkorra.rpg.ProjectKorraRPG;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.listeners.WorldEventScheduleListener;
-import com.projectkorra.rpg.modules.worldevents.schedule.storage.ScheduleStorage;
-import org.bukkit.event.HandlerList;
-import org.bukkit.plugin.Plugin;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-public class WorldEventScheduler {
- private final Plugin plugin = ProjectKorraRPG.getPlugin();
- private final Map scheduledEvents = new ConcurrentHashMap<>();
- private WorldEventScheduleListener worldEventScheduleListener;
- private final ScheduleStorage scheduleStorage;
-
- public WorldEventScheduler(WorldEventScheduleListener worldEventScheduleListener, ScheduleStorage scheduleStorage) {
- this.worldEventScheduleListener = worldEventScheduleListener;
- this.scheduleStorage = scheduleStorage;
-
- initSchedules();
- }
-
- /**
- * Initialize all world event schedules
- */
- private void initSchedules() {
- // Clean up any existing strategies before initializing
- cleanup();
-
- this.plugin.getLogger().info("Initializing WorldEventScheduler...");
-
- // Process each registered WorldEvent
- for (WorldEvent event : WorldEvent.getAllEvents().values()) {
- try {
- scheduleEvent(event);
- } catch (Exception e) {
- this.plugin.getLogger().severe("Failed to schedule event: " + event.getKey() + e.getMessage());
- }
- }
- }
-
- /**
- * Schedule a single world event
- */
- private void scheduleEvent(WorldEvent event) {
- cancelEvent(event);
-
- // Create a new strategy based on WorldEvent configuration
- WorldEventScheduleStrategy scheduleStrategy = WorldEventScheduleStrategyFactory.get(event.getConfig(), this.scheduleStorage);
-
- // Store the event context
- ScheduledEventContext context = new ScheduledEventContext(event, scheduleStrategy);
- this.scheduledEvents.put(event, context);
-
- this.plugin.getLogger().info("Scheduling event: " + event.getKey() + " with strategy: " + scheduleStrategy.getClass().getSimpleName());
-
- // Start the schedule
- scheduleStrategy.scheduleNext(event, this.plugin);
- context.setActive(true);
- }
-
- /**
- * Reschedule an event after it has stopped
- */
- public void rescheduleEvent(WorldEvent event) {
- ScheduledEventContext context = this.scheduledEvents.get(event);
- if (context != null) {
- plugin.getLogger().info("Rescheduling event: " + event.getKey());
- context.getStrategy().scheduleNext(context.getEvent(), this.plugin);;
- }
- }
-
- /**
- * Cancel a scheduling for a specific event
- */
- public void cancelEvent(WorldEvent event) {
- ScheduledEventContext context = this.scheduledEvents.get(event);
- if (context != null) {
- context.getStrategy().cancelSchedule();
- context.setActive(false);
- plugin.getLogger().info("Cancelled schedule for event: " + event.getKey());
- }
- }
-
- /**
- * Set an event as active or inactive
- */
- public void setEventActive(WorldEvent event, boolean active) {
- ScheduledEventContext context = this.scheduledEvents.get(event);
- if (context != null) {
- context.setActive(active);
- }
- }
-
- /**
- * Check if an event is currently scheduled
- */
- public boolean isEventScheduled(WorldEvent event) {
- ScheduledEventContext context = this.scheduledEvents.get(event);
- return context != null && context.isActive();
- }
-
- /**
- * Clean up all scheduled events
- */
- public void cleanup() {
- this.plugin.getLogger().info("Cleaning up WorldEventScheduler...");
-
- // Cancel all existing strategies
- for (ScheduledEventContext context : scheduledEvents.values()) {
- try {
- context.getStrategy().cancelSchedule();
- } catch (Exception e) {
- this.plugin.getLogger().severe("Failed to cancel strategy for event: " + context.getEvent().getKey() + e.getMessage());
- }
- }
-
- scheduledEvents.clear();
-
- // Unregister listener
- if (this.worldEventScheduleListener != null) {
- HandlerList.unregisterAll(this.worldEventScheduleListener);
- this.worldEventScheduleListener = null;
- }
- }
-
- public Plugin getPlugin() {
- return plugin;
- }
-
- public Map getScheduledEvents() {
- return scheduledEvents;
- }
-
- public WorldEventScheduleListener getWorldEventScheduleListener() {
- return worldEventScheduleListener;
- }
-
- public ScheduleStorage getScheduleStorage() {
- return scheduleStorage;
- }
-
- /**
- * ScheduledEventContext keeps all scheduling information about a single world event
- */
- private static class ScheduledEventContext {
- private final WorldEvent event;
- private final WorldEventScheduleStrategy strategy;
- private boolean active = false;
-
- public ScheduledEventContext(WorldEvent event, WorldEventScheduleStrategy strategy) {
- this.event = event;
- this.strategy = strategy;
- }
-
- public WorldEvent getEvent() {
- return event;
- }
-
- public WorldEventScheduleStrategy getStrategy() {
- return strategy;
- }
-
- public boolean isActive() {
- return active;
- }
-
- public void setActive(boolean active) {
- this.active = active;
- }
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/storage/ScheduleStorage.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/storage/ScheduleStorage.java
deleted file mode 100644
index 5cecf34..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/storage/ScheduleStorage.java
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule.storage;
-
-import com.projectkorra.projectkorra.storage.DBConnection;
-import com.projectkorra.rpg.ProjectKorraRPG;
-
-import java.sql.*;
-import java.time.Instant;
-import java.util.Optional;
-import java.util.logging.Level;
-
-import static com.projectkorra.rpg.storage.TableCreator.RPG_SCHEDULE_TABLE;
-
-public class ScheduleStorage extends DBConnection {
- /**
- * Gets the last time a specific world event was triggered.
- *
- * @param worldEventKey The unique key for the world event
- * @return Optional containing the last trigger time, or empty if never triggered
- * @throws SQLException if a database error occurs
- */
- public Optional getLastTriggeredTime(String worldEventKey) throws SQLException {
- try {
- Connection conn = sql.getConnection();
-
- if (conn == null) {
- throw new SQLException("Could not get database connection");
- }
-
- final String query = "SELECT last_triggered FROM " + RPG_SCHEDULE_TABLE + " WHERE worldevent = ?";
- PreparedStatement ps = conn.prepareStatement(query);
- ps.setString(1, worldEventKey);
-
- ResultSet rs = ps.executeQuery();
-
- if (rs.next()) {
- Timestamp timestamp = rs.getTimestamp("last_triggered");
- return Optional.of(timestamp.toInstant());
- }
-
- // If there's no last trigger time, return empty
- return Optional.empty();
- } catch (SQLException e) {
- ProjectKorraRPG.getPlugin().getLogger().log(Level.SEVERE, "Failed to get last triggered time for event: " + worldEventKey, e);
- throw e;
- }
- }
-
- /**
- * Updates the last triggered time for a specific world event.
- * Creates a new record if the world event doesn't exist in the database.
- *
- * @param worldEventKey The unique key for the world event
- * @param triggerTime The time when the event was triggered (null for current time)
- * @throws SQLException if a database error occurs
- */
- public void updateLastTriggeredTime(String worldEventKey, Instant triggerTime) throws SQLException {
- Instant timeToRecord = (triggerTime != null) ? triggerTime : Instant.now();
- PreparedStatement updatePs;
-
- try {
- Connection conn = sql.getConnection();
-
- if (conn == null) {
- throw new SQLException("Could not get database connection");
- }
-
- // First, check if the event record exists
- PreparedStatement checkPs = conn.prepareStatement("SELECT id FROM " + RPG_SCHEDULE_TABLE + " WHERE worldevent = ?");
- checkPs.setString(1, worldEventKey);
- ResultSet rs = checkPs.executeQuery();
-
- boolean recordExists = rs.next();
-
- // Close the result set and first statement before creating another
- rs.close();
- checkPs.close();
-
- if (recordExists) {
- // Update existing record
- updatePs = conn.prepareStatement("UPDATE " + RPG_SCHEDULE_TABLE + " SET last_triggered = ? WHERE worldevent = ?");
- updatePs.setTimestamp(1, Timestamp.from(timeToRecord));
- updatePs.setString(2, worldEventKey);
-
- updatePs.executeUpdate();
- } else {
- // Insert a new record
- updatePs = conn.prepareStatement("INSERT INTO " + RPG_SCHEDULE_TABLE + " (worldevent, last_triggered) VALUES (?, ?)");
- updatePs.setString(1, worldEventKey);
- updatePs.setTimestamp(2, Timestamp.from(timeToRecord));
-
- updatePs.executeUpdate();
- }
- } catch (SQLException e) {
- ProjectKorraRPG.getPlugin().getLogger().log(Level.SEVERE,"Failed to update last triggered time for event: " + worldEventKey, e);
- throw e;
- }
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryInGameDaysStrategy.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryInGameDaysStrategy.java
deleted file mode 100644
index 6af6674..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryInGameDaysStrategy.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule.strategies;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.schedule.WorldEventScheduleStrategy;
-import com.projectkorra.rpg.modules.worldevents.schedule.storage.ScheduleStorage;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalTime;
-
-public class EveryInGameDaysStrategy implements WorldEventScheduleStrategy {
- private final LocalTime timeOfDay;
- private final Duration repeatDuration;
- private final Duration offsetDuration;
- private final double chance;
- private final Duration cooldownDuration;
- private final ScheduleStorage storage;
-
- private BukkitTask task;
- private Instant lastTriggerTime = null;
-
- public EveryInGameDaysStrategy(LocalTime timeOfDay, Duration repeatDuration, Duration offsetDuration, double chance, Duration cooldownDuration, ScheduleStorage storage) {
- this.timeOfDay = timeOfDay;
- this.repeatDuration = repeatDuration;
- this.offsetDuration = offsetDuration;
- this.chance = chance;
- this.cooldownDuration = cooldownDuration;
- this.storage = storage;
- }
-
- @Override
- public void scheduleNext(WorldEvent event, Plugin plugin) {
- //TODO: Implement scheduling
- }
-
- @Override
- public void cancelSchedule() {
- if (task != null && !task.isCancelled()) {
- task.cancel();
- task = null;
- }
- }
-
- public LocalTime getTimeOfDay() {
- return timeOfDay;
- }
-
- public Duration getRepeatDuration() {
- return repeatDuration;
- }
-
- public Duration getOffsetDuration() {
- return offsetDuration;
- }
-
- public double getChance() {
- return chance;
- }
-
- public Duration getCooldownDuration() {
- return cooldownDuration;
- }
-
- public ScheduleStorage getStorage() {
- return storage;
- }
-
- public BukkitTask getTask() {
- return task;
- }
-
- public Instant getLastTriggerTime() {
- return lastTriggerTime;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryRealWorldDaysStrategy.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryRealWorldDaysStrategy.java
deleted file mode 100644
index 159e215..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/EveryRealWorldDaysStrategy.java
+++ /dev/null
@@ -1,218 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule.strategies;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.schedule.WorldEventScheduleStrategy;
-import com.projectkorra.rpg.modules.worldevents.schedule.storage.ScheduleStorage;
-import org.bukkit.plugin.Plugin;
-import org.bukkit.scheduler.BukkitRunnable;
-import org.bukkit.scheduler.BukkitTask;
-
-import java.sql.SQLException;
-import java.time.*;
-import java.util.Optional;
-import java.util.Random;
-
-public class EveryRealWorldDaysStrategy implements WorldEventScheduleStrategy {
- private final LocalTime targetTime;
- private final Duration repeatInterval;
- private final Duration maxOffset;
- private final double chance;
- private final Duration cooldown;
-
- private final Random random = new Random();
- private BukkitTask task;
- private final ScheduleStorage storage;
-
- /**
- * Creates a new real-time world event scheduling strategy.
- *
- * @param targetTime The time of day when the event should trigger (e.g., 7:00 AM)
- * @param repeatInterval How often to check for triggering the event (e.g., every 7 days)
- * @param maxOffset Maximum random time offset to apply (e.g., up to 3 days and 12 hours)
- * @param chance Probability of the event triggering when conditions are met (0.0-1.0)
- * @param cooldown Minimum time between event triggers (e.g., 60 days)
- */
- public EveryRealWorldDaysStrategy(LocalTime targetTime, Duration repeatInterval, Duration maxOffset, double chance, Duration cooldown, ScheduleStorage storage) {
- this.targetTime = targetTime;
- this.repeatInterval = repeatInterval;
- this.maxOffset = maxOffset;
- this.chance = chance;
- this.cooldown = cooldown;
- this.storage = storage;
- }
-
- @Override
- public void scheduleNext(WorldEvent event, Plugin plugin) {
- cancelSchedule();
-
- String eventKey = event.getKey();
- LocalDateTime now = LocalDateTime.now();
- LocalDateTime nextTime;
-
- try {
- // Get last trigger time
- Optional lastTriggerTime = storage.getLastTriggeredTime(eventKey);
-
- // Calculate next trigger time
- nextTime = calculateNextTriggeerTime(now, lastTriggerTime);
-
- // Apply random offset
- if (!maxOffset.isZero() || !maxOffset.isNegative()) {
- long offsetMillis = (long) (random.nextDouble() * maxOffset.toMillis());
- nextTime = nextTime.plus(Duration.ofMillis(offsetMillis));
- plugin.getLogger().info("Applied random offset of " + formatDuration(Duration.ofMillis(offsetMillis)) + " to event " + eventKey);
- }
-
- // Ensure no re-schedule in past
- if (nextTime.isBefore(now)) {
- nextTime = now.plusSeconds(30);
- plugin.getLogger().info("Adjusted schedule time for " + eventKey + " to be in the future");
- }
-
- // Actually schedule the event
- long delayTicks = Math.max(100, Duration.between(now, nextTime).toMillis() / 50);
- plugin.getLogger().info("Scheduling " + eventKey + " for " + nextTime + " (in " + formatDuration(Duration.ofMillis(delayTicks * 50)) + ")");
-
- task = new BukkitRunnable() {
- @Override
- public void run() {
- tryTriggerEvent(event, plugin);
- }
- }.runTaskLater(plugin, delayTicks);
-
- } catch (Exception e) {
- plugin.getLogger().severe("Error scheduling " + eventKey + "\n" + e.getMessage());
-
- // Schedule for retry in 5 minutes
- task = new BukkitRunnable() {
- @Override
- public void run() {
- scheduleNext(event, plugin);
- }
- }.runTaskLater(plugin, 6000);
- }
- }
-
- private void tryTriggerEvent(WorldEvent event, Plugin plugin) {
- String eventKey = event.getKey();
- boolean onCooldown = false;
-
- try {
- // Check cooldown
- Optional lastTriggerTime = storage.getLastTriggeredTime(eventKey);
-
- if (lastTriggerTime.isPresent()) {
- Duration sinceLastTrigger = Duration.between(lastTriggerTime.get(), Instant.now());
- if (sinceLastTrigger.compareTo(cooldown) < 0) {
- onCooldown = true;
- plugin.getLogger().info("Event " + eventKey + " is on cooldown for " + formatDuration(sinceLastTrigger));
- }
- }
-
- // Check chance and cooldown
- if (!onCooldown && random.nextDouble() <= chance) {
- // start event
- plugin.getLogger().info("Starting " + eventKey + " (passed " + (chance * 100) + "% chance check)");
-
- try {
- storage.updateLastTriggeredTime(eventKey, Instant.now());
- } catch (SQLException e) {
- plugin.getLogger().severe("Failed to update last trigger time for event: " + eventKey + "\n" + e.getMessage());
- }
-
- event.startEvent();
- } else if (!onCooldown) {
- plugin.getLogger().info("Event " + eventKey + " failed " + (chance * 100) + "% chance check");
- }
-
- } catch (Exception e) {
- plugin.getLogger().severe("Error checking/triggering " + eventKey + "\n" + e.getMessage());
- }
-
- // Always schedule the next occurrence
- scheduleNext(event, plugin);
- }
-
- private LocalDateTime calculateNextTriggeerTime(LocalDateTime now, Optional lastTriggerTime) {
- // Start with today at target time
- LocalDateTime candidate = LocalDateTime.of(now.toLocalDate(), targetTime);
-
- // If that time has passed today, move to tomorrow
- if (candidate.isBefore(now)) {
- candidate = candidate.plusDays(1);
- }
-
- // If last trigger and cooldown, respect it
- if (lastTriggerTime.isPresent()) {
- LocalDateTime lastTriggerDateTime = LocalDateTime.ofInstant(lastTriggerTime.get(), ZoneId.systemDefault());
- LocalDateTime earliestAllowed = lastTriggerDateTime.plus(cooldown);
-
- while (candidate.isBefore(earliestAllowed)) {
- candidate = candidate.plus(repeatInterval);
- }
- }
-
- return candidate;
- }
-
- @Override
- public void cancelSchedule() {
- if (task != null && !task.isCancelled()) {
- task.cancel();
- task = null;
- }
- }
-
- private String formatDuration(Duration duration) {
- long days = duration.toDays();
- duration = duration.minusDays(days);
-
- long hours = duration.toHours();
- duration = duration.minusHours(hours);
-
- long minutes = duration.toMinutes();
- duration = duration.minusMinutes(minutes);
-
- long seconds = duration.getSeconds();
-
- StringBuilder sb = new StringBuilder();
- if (days > 0) sb.append(days).append("d ");
- if (hours > 0) sb.append(hours).append("h ");
- if (minutes > 0) sb.append(minutes).append("m ");
- if (seconds > 0 || (days == 0 && hours == 0 && minutes == 0)) sb.append(seconds).append("s");
-
- return sb.toString().trim();
- }
-
- public LocalTime getTargetTime() {
- return targetTime;
- }
-
- public Duration getRepeatInterval() {
- return repeatInterval;
- }
-
- public Duration getMaxOffset() {
- return maxOffset;
- }
-
- public double getChance() {
- return chance;
- }
-
- public Duration getCooldown() {
- return cooldown;
- }
-
- public Random getRandom() {
- return random;
- }
-
- public BukkitTask getTask() {
- return task;
- }
-
- public ScheduleStorage getStorage() {
- return storage;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/util/ScheduleType.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/util/ScheduleType.java
deleted file mode 100644
index 8f5db44..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/schedule/strategies/util/ScheduleType.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.schedule.strategies.util;
-
-public enum ScheduleType {
- REAL_DAYS,
- IN_GAME_DAYS;
-
- public static ScheduleType fromString(String name) {
- if (name == null) {
- return null;
- }
-
- String lowercaseName = name.toLowerCase();
-
- // Regex for REAL_DAYS to cover all possible variations
- if (lowercaseName.matches("^real[-_\\s]?(?:world[-_\\s]?)?(?:days?|time|hours?|minutes?)$")) {
- return REAL_DAYS;
- }
-
- // Regex for IN_GAME_DAYS to cover all possible variations
- if (lowercaseName.matches("^(?:in[-_\\s]?)?(?:game|mc|minecraft)[-_\\s]?(?:days?|time|hours?|minutes?)$")) {
- return IN_GAME_DAYS;
- }
-
- return null;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventModificationService.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventModificationService.java
new file mode 100644
index 0000000..8e1c3ac
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventModificationService.java
@@ -0,0 +1,71 @@
+package com.projectkorra.rpg.modules.worldevents.service;
+
+import com.projectkorra.projectkorra.attribute.AttributeModification;
+import com.projectkorra.projectkorra.attribute.AttributeModifier;
+import com.projectkorra.projectkorra.attribute.AttributeUtil;
+import com.projectkorra.projectkorra.event.AbilityRecalculateAttributeEvent;
+import com.projectkorra.rpg.ProjectKorraRPG;
+import com.projectkorra.rpg.modules.worldevents.models.AttributeRules;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.storage.ActiveWorldEventIndex;
+import commonslang3.projectkorra.lang3.tuple.Pair;
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
+
+/**
+ * A service class responsible for applying world-event-based attribute modifications to abilities.
+ * It processes active world events and applies relevant attribute modifications configured within
+ * the world event's configuration file to the abilities based on their context (element, ability name, attribute name)
+ * Has to listen to the {@link AbilityRecalculateAttributeEvent} to function.
+ */
+public final class WorldEventModificationService {
+ private final ActiveWorldEventIndex activeEventsIndex;
+
+ public WorldEventModificationService(final ActiveWorldEventIndex activeEventsIndex) {
+ this.activeEventsIndex = activeEventsIndex;
+ }
+
+ /**
+ * Applies modifications from active WorldEvents to the abilities / elements configured in the corresponding config
+ */
+ public void applyWorldEventMods(AbilityRecalculateAttributeEvent event, World world) {
+ final String element = event.getAbility().getElement().getName();
+ final String ability = event.getAbility().getName();
+ final String attribute = event.getAttribute();
+
+ for (WorldEvent worldEvent : activeEventsIndex.getActiveIn(world)) {
+ AttributeRules rules = worldEvent.getAttributeRules();
+ if (rules == null) continue;
+
+ Object raw = rules.find(element, ability, attribute);
+ if (raw == null) continue;
+
+ AttributeModification mod = buildModification(raw, worldEvent.getKey());
+ if (mod != null) event.addModification(mod);
+ }
+ }
+
+ private AttributeModification buildModification(Object raw, NamespacedKey key) {
+ if (raw instanceof Boolean b) {
+ return AttributeModification.setter(b, AttributeModification.PRIORITY_NORMAL, key);
+ }
+
+ if (raw instanceof Number n) {
+ return AttributeModification.of(AttributeModifier.SET, n, AttributeModification.PRIORITY_NORMAL, key);
+ }
+
+ String rawStr = raw.toString().replace(" ", "");
+ Pair parsed = AttributeUtil.getModification(rawStr);
+
+ if (parsed != null) {
+ return AttributeModification.of(parsed.getLeft(), parsed.getRight(), AttributeModification.PRIORITY_NORMAL, key);
+ }
+
+ ProjectKorraRPG.getPlugin().getLogger().warning("WorldEvent parse failed for key:" + key.getKey() + " raw:" + rawStr);
+ return null;
+ }
+
+ public ActiveWorldEventIndex getActiveEventsIndex() {
+ return activeEventsIndex;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventService.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventService.java
new file mode 100644
index 0000000..29df9e9
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/service/WorldEventService.java
@@ -0,0 +1,216 @@
+package com.projectkorra.rpg.modules.worldevents.service;
+
+import com.projectkorra.projectkorra.ability.CoreAbility;
+import com.projectkorra.rpg.ProjectKorraRPG;
+import com.projectkorra.rpg.modules.worldevents.event.WorldEventStartEvent;
+import com.projectkorra.rpg.modules.worldevents.event.WorldEventStopEvent;
+import com.projectkorra.rpg.modules.worldevents.models.ActiveWorldEvent;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import com.projectkorra.rpg.modules.worldevents.storage.ActiveWorldEventIndex;
+import org.bukkit.Bukkit;
+import org.bukkit.World;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
+import org.bukkit.scheduler.BukkitTask;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public final class WorldEventService {
+ private final ProjectKorraRPG plugin;
+ private final ActiveWorldEventIndex activeEventsIndex;
+
+ // For non ticking events (No BossBar usage)
+ private final Map nonTickingStops = new HashMap<>();
+
+ // Global ticker so the BossBar for each ActiveWorldEvent receives a tick update
+ private BukkitTask ticker;
+ private long tickNo;
+
+ public WorldEventService(ProjectKorraRPG plugin, ActiveWorldEventIndex activeEventsIndex) {
+ this.plugin = plugin;
+ this.activeEventsIndex = activeEventsIndex;
+ }
+
+ public boolean start(WorldEvent worldEvent, World runtimeWorld) {
+ if (worldEvent == null || runtimeWorld == null) return false;
+
+ if (activeEventsIndex.contains(worldEvent)) {
+ plugin.getLogger().warning("WorldEvent already running: " + worldEvent.getKey());
+ return false;
+ }
+ if (worldEvent.isWorldDisabled(runtimeWorld)) {
+ plugin.getLogger().info("Cannot start WorldEvent " + worldEvent.getKey() + " world is null or disabled");
+ return false;
+ }
+
+ ActiveWorldEvent active = new ActiveWorldEvent(worldEvent, runtimeWorld);
+
+ // Publish to index
+ activeEventsIndex.add(worldEvent, active);
+
+ // Start displays / Set start time for ticker
+ active.start();
+
+ // Recalc ability attributes
+ recalcAllAbilities();
+
+ if (active.requiresTicking()) {
+ ensureTicker(); // TaskTimer because of BossBar
+ } else {
+ scheduleNonTickingStop(worldEvent); // Scheduler to prevent useless ticking without given BossBar
+ }
+
+ Bukkit.getPluginManager().callEvent(new WorldEventStartEvent(worldEvent));
+ return true;
+ }
+
+ /**
+ * Temp method, will lead to issues with coming Scheduler
+ */
+ public boolean start(WorldEvent worldEvent) {
+ World runtimeWorld = null;
+
+ if (worldEvent != null && !worldEvent.getScheduledWorlds().isEmpty()) {
+ runtimeWorld = worldEvent.getScheduledWorlds().getFirst();
+ }
+ if (runtimeWorld == null && !plugin.getServer().getWorlds().isEmpty()) {
+ runtimeWorld = plugin.getServer().getWorlds().getFirst();
+ }
+
+ return start(worldEvent, runtimeWorld);
+ }
+
+ public boolean stop(WorldEvent worldEvent) {
+ BukkitTask delayed = nonTickingStops.remove(worldEvent);
+ if (delayed != null) delayed.cancel();
+
+ ActiveWorldEvent active = activeEventsIndex.remove(worldEvent);
+ if (active == null) {
+ plugin.getLogger().warning("Tried to stop non running WorldEvent: " + worldEvent.getKey());
+ return false;
+ }
+
+ // Stop displays / clear viewers
+ active.stop();
+
+ // Stop ticker
+ tryStopTicker();
+
+ // Recalc ability attributes
+ recalcAllAbilities();
+
+ Bukkit.getPluginManager().callEvent(new WorldEventStopEvent(worldEvent));
+ return true;
+ }
+
+ public void stopAll() {
+ for (BukkitTask task : nonTickingStops.values()) {
+ task.cancel();
+ }
+ nonTickingStops.clear();
+
+ for (WorldEvent worldEvent : new ArrayList<>(activeEventsIndex.activeWorldEvents())) {
+ stop(worldEvent);
+ }
+ }
+
+ public void shutdown() {
+ stopAll();
+ if (ticker != null) {
+ ticker.cancel();
+ ticker = null;
+ }
+ }
+
+ private void recalcAllAbilities() {
+ for (CoreAbility ability : CoreAbility.getAbilitiesByInstances()) {
+ ability.recalculateAttributes();
+ }
+ }
+
+ public void addViewer(WorldEvent worldEvent, Player viewer) {
+ if (worldEvent == null) return;
+ ActiveWorldEvent active = activeEventsIndex.getActive(worldEvent);
+
+ if (active != null) {
+ active.addViewer(viewer);
+ }
+ }
+
+ public void removeViewer(WorldEvent worldEvent, Player viewer) {
+ if (worldEvent == null) return;
+ ActiveWorldEvent active = activeEventsIndex.getActive(worldEvent);
+
+ if (active != null) {
+ active.removeViewer(viewer);
+ }
+ }
+
+ public void sendWorldEventRunningMessage(WorldEvent worldEvent, Player player) {
+ if (worldEvent == null) return;
+ ActiveWorldEvent active = activeEventsIndex.getActive(worldEvent);
+
+ if (active != null) {
+ active.sendWorldEventRunningMessage(player);
+ }
+ }
+
+ private void scheduleNonTickingStop(WorldEvent worldEvent) {
+ long delayTicks = Math.max(1L, worldEvent.getDuration() / 50L); // Convert milliseconds to ticks
+ BukkitTask task = new BukkitRunnable() {
+ @Override
+ public void run() {
+ stop(worldEvent);
+ }
+ }.runTaskLater(plugin, delayTicks);
+
+ nonTickingStops.put(worldEvent, task);
+ }
+
+ private void ensureTicker() {
+ if (ticker != null || !activeEventsIndex.hasTicking()) return;
+
+ tickNo = 0;
+ ticker = new BukkitRunnable() {
+ @Override
+ public void run() {
+ tickNo++;
+ long now = System.currentTimeMillis();
+
+ // Use snapshot to iterate to defend against mutation in some cases
+ ActiveWorldEvent[] snapshot = activeEventsIndex.snapshot();
+ if (snapshot.length == 0) {
+ tryStopTicker();
+ return;
+ }
+
+ List expired = null;
+ for (ActiveWorldEvent active : snapshot) {
+ if (active.tick(tickNo, now)) {
+ if (expired == null) expired = new ArrayList<>();
+ expired.add(active.getWorldEvent());
+ }
+ }
+ if (expired != null) {
+ for (WorldEvent worldEvent : expired) {
+ stop(worldEvent);
+ }
+ }
+ }
+ }.runTaskTimer(plugin, 1L, 1L);
+ }
+
+ private void tryStopTicker() {
+ if (activeEventsIndex.isEmpty() && ticker != null) {
+ ticker.cancel();
+ ticker = null;
+ }
+ }
+
+ public ActiveWorldEventIndex getActiveEventsIndex() {
+ return activeEventsIndex;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/ActiveWorldEventIndex.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/ActiveWorldEventIndex.java
new file mode 100644
index 0000000..454b969
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/ActiveWorldEventIndex.java
@@ -0,0 +1,97 @@
+package com.projectkorra.rpg.modules.worldevents.storage;
+
+import com.projectkorra.rpg.modules.worldevents.models.ActiveWorldEvent;
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import org.bukkit.World;
+
+import java.util.*;
+
+public final class ActiveWorldEventIndex {
+ private final Map activeWorldEventMap = new HashMap<>();
+ private final Map> activeWorldEventsByWorld = new HashMap<>();
+
+ // Volatile to make ticker [WorldEventService] always see latest published array without locking
+ private volatile ActiveWorldEvent[] snapshot = new ActiveWorldEvent[0];
+
+ public boolean contains(WorldEvent worldEvent) {
+ return activeWorldEventMap.containsKey(worldEvent);
+ }
+
+ public void add(WorldEvent worldEvent, ActiveWorldEvent active) {
+ activeWorldEventMap.put(worldEvent, active);
+ Set set = activeWorldEventsByWorld.computeIfAbsent(active.getRuntimeWorld().getUID(), k -> new HashSet<>());
+ set.add(worldEvent);
+
+ rebuildSnapshot();
+ }
+
+ public ActiveWorldEvent remove(WorldEvent worldEvent) {
+ ActiveWorldEvent removed = activeWorldEventMap.remove(worldEvent);
+ if (removed != null) {
+ UUID worldId = removed.getRuntimeWorld().getUID();
+ Set set = activeWorldEventsByWorld.get(worldId);
+ if (set != null) {
+ set.remove(worldEvent);
+ if (set.isEmpty()) activeWorldEventsByWorld.remove(worldId);
+ }
+ rebuildSnapshot();
+ }
+ return removed;
+ }
+
+ public boolean hasTicking() {
+ for (ActiveWorldEvent active : activeWorldEventMap.values()) {
+ if (active.requiresTicking()) return true;
+ }
+ return false;
+ }
+
+ public ActiveWorldEvent[] tickingSnapshot() {
+ List list = new ArrayList<>(activeWorldEventMap.size());
+ for (ActiveWorldEvent active : activeWorldEventMap.values()) {
+ if (active.requiresTicking()) {
+ list.add(active);
+ }
+ }
+ return list.toArray(ActiveWorldEvent[]::new);
+ }
+
+ public void clearAll() {
+ activeWorldEventMap.clear();
+ activeWorldEventsByWorld.clear();
+ snapshot = new ActiveWorldEvent[0];
+ }
+
+ public boolean isEmpty() {
+ return activeWorldEventMap.isEmpty();
+ }
+
+ public Set activeWorldEvents() {
+ return Collections.unmodifiableSet(activeWorldEventMap.keySet());
+ }
+
+ public Set getActiveIn(World world) {
+ if (world == null) return Collections.emptySet();
+ Set set = activeWorldEventsByWorld.get(world.getUID());
+ return set == null ? Collections.emptySet() : Collections.unmodifiableSet(set);
+ }
+
+ public boolean hasActiveIn(World world) {
+ Set worldEventsInWorld = activeWorldEventsByWorld.get(world.getUID());
+ return worldEventsInWorld != null && !worldEventsInWorld.isEmpty();
+ }
+
+ public ActiveWorldEvent getActive(WorldEvent spec) {
+ return activeWorldEventMap.get(spec);
+ }
+
+ public ActiveWorldEvent[] snapshot() {
+ return snapshot;
+ }
+
+ private void rebuildSnapshot() {
+ Collection values = activeWorldEventMap.values();
+ ActiveWorldEvent[] array = new ActiveWorldEvent[values.size()];
+ snapshot = values.toArray(array);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/WorldEventRegistry.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/WorldEventRegistry.java
new file mode 100644
index 0000000..36e71ec
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/storage/WorldEventRegistry.java
@@ -0,0 +1,44 @@
+package com.projectkorra.rpg.modules.worldevents.storage;
+
+import com.projectkorra.rpg.modules.worldevents.models.WorldEvent;
+import org.bukkit.NamespacedKey;
+
+import java.util.*;
+
+public final class WorldEventRegistry {
+ private final Map loadedWorldEvents = new HashMap<>();
+ private final Map byPath = new HashMap<>();
+
+ public void register(WorldEvent worldEvent) {
+ if (worldEvent == null) return;
+ loadedWorldEvents.put(worldEvent.getKey(), worldEvent);
+ byPath.put(worldEvent.getKey().getKey().toLowerCase(Locale.ROOT), worldEvent.getKey());
+ }
+
+ public void registerAll(Collection worldEvents) {
+ if (worldEvents == null) return;
+ for (WorldEvent worldEvent : worldEvents) {
+ register(worldEvent);
+ }
+ }
+
+ public void clear() {
+ loadedWorldEvents.clear();
+ byPath.clear();
+ }
+
+ public Optional findByKey(NamespacedKey key) {
+ if (key == null) return Optional.empty();
+ return Optional.ofNullable(loadedWorldEvents.get(key));
+ }
+
+ public Optional findByPath(String path) {
+ if (path == null) return Optional.empty();
+ NamespacedKey key = byPath.get(path.toLowerCase(Locale.ROOT));
+ return key == null ? Optional.empty() : Optional.ofNullable(loadedWorldEvents.get(key));
+ }
+
+ public Map getAll() {
+ return Collections.unmodifiableMap(loadedWorldEvents);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/BossBarCleanup.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/BossBarCleanup.java
new file mode 100644
index 0000000..372d4a6
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/BossBarCleanup.java
@@ -0,0 +1,26 @@
+package com.projectkorra.rpg.modules.worldevents.util;
+
+import org.bukkit.Bukkit;
+import org.bukkit.NamespacedKey;
+import org.bukkit.boss.KeyedBossBar;
+import org.bukkit.plugin.Plugin;
+
+import java.util.Iterator;
+import java.util.Locale;
+
+public class BossBarCleanup {
+ private BossBarCleanup() {}
+
+ public static void removeAllFor(Plugin plugin) {
+ String ns = plugin.getName().toLowerCase(Locale.ROOT);
+ Iterator< KeyedBossBar> it = Bukkit.getBossBars();
+
+ while (it.hasNext()) {
+ KeyedBossBar bar = it.next();
+ NamespacedKey key = bar.getKey();
+ if (ns.equals(key.getNamespace())) {
+ Bukkit.removeBossBar(key);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/DisplayHelper.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/DisplayHelper.java
deleted file mode 100644
index d059d92..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/DisplayHelper.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util;
-
-import org.bukkit.boss.BarColor;
-import org.bukkit.boss.BarStyle;
-
-public class DisplayHelper {
- public static BarColor convertStringToBarColor(String colorStr) {
- if (colorStr == null) {
- return BarColor.RED;
- }
-
- return switch (colorStr.toUpperCase()) {
- case "GREEN" -> BarColor.GREEN;
- case "BLUE" -> BarColor.BLUE;
- case "YELLOW" -> BarColor.YELLOW;
- case "PURPLE" -> BarColor.PURPLE;
- case "WHITE" -> BarColor.WHITE;
- case "PINK" -> BarColor.PINK;
-
- default -> BarColor.RED;
- };
- }
-
- public static BarStyle convertStringToBarStyle(String styleStr) {
- if (styleStr == null) {
- return BarStyle.SOLID;
- }
-
- return switch (styleStr.toUpperCase()) {
- case "SEGMENTED_6" -> BarStyle.SEGMENTED_6;
- case "SEGMENTED_10" -> BarStyle.SEGMENTED_10;
- case "SEGMENTED_12" -> BarStyle.SEGMENTED_12;
- case "SEGMENTED_20" -> BarStyle.SEGMENTED_20;
-
- default -> BarStyle.SOLID;
- };
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/ScheduleParser.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/ScheduleParser.java
new file mode 100644
index 0000000..aeb3bc5
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/ScheduleParser.java
@@ -0,0 +1,52 @@
+package com.projectkorra.rpg.modules.worldevents.util;
+
+import java.time.Duration;
+import java.time.LocalTime;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class ScheduleParser {
+ private static final Pattern TIME = Pattern.compile("(\\d{1,2})(?::(\\d{2}))?(am|pm)?", Pattern.CASE_INSENSITIVE);
+ private static final Pattern DURATION = Pattern.compile("(\\d+)([dhms])", Pattern.CASE_INSENSITIVE);
+
+ private ScheduleParser() {}
+
+ public static LocalTime parseTimeOfDay(String raw, LocalTime fallback) {
+ if (raw == null || raw.isBlank()) return fallback;
+ String s = raw.trim().toLowerCase(Locale.ROOT);
+ Matcher m = TIME.matcher(s);
+ if (!m.matches()) return fallback;
+
+ int hour = Integer.parseInt(m.group(1));
+ int minute = m.group(2) != null ? Integer.parseInt(m.group(2)) : 0;
+ String period = m.group(3);
+
+ if ("pm".equals(period) && hour < 12) hour += 12;
+ if ("am".equals(period) && hour == 12) hour = 0;
+
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return fallback;
+
+ return LocalTime.of(hour, minute);
+ }
+
+ public static Duration parseDuration(String raw, Duration fallback) {
+ if (raw == null || raw.isBlank()) return fallback;
+
+ Matcher m = DURATION.matcher(raw.trim().toLowerCase(Locale.ROOT));
+ long seconds = 0L;
+ boolean any = false;
+
+ while (m.find()) {
+ any = true;
+ long value = Long.parseLong(m.group(1));
+ String unit = m.group(2);
+ if ("d".equals(unit)) seconds += value * 86400L;
+ else if ("h".equals(unit)) seconds += value * 3600L;
+ else if ("m".equals(unit)) seconds += value * 60L;
+ else if ("s".equals(unit)) seconds += value;
+ }
+
+ return any ? Duration.ofSeconds(seconds) : fallback;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/IWorldEventDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/IWorldEventDisplay.java
deleted file mode 100644
index 61e8ded..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/IWorldEventDisplay.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-
-public interface IWorldEventDisplay {
- void startDisplay(WorldEvent event);
- void updateDisplay(WorldEvent event, double progress);
- void stopDisplay(WorldEvent event);
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/BossBarDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/BossBarDisplay.java
deleted file mode 100644
index 1935779..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/BossBarDisplay.java
+++ /dev/null
@@ -1,75 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display.bossbar;
-
-import com.projectkorra.projectkorra.util.ChatUtil;
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.util.display.IWorldEventDisplay;
-import org.bukkit.Bukkit;
-import org.bukkit.boss.BarColor;
-import org.bukkit.boss.BarStyle;
-import org.bukkit.entity.Player;
-
-public class BossBarDisplay implements IWorldEventDisplay {
- private final WorldEventBossBar worldEventBossBar;
- private final BarColor barColor;
- private final BarStyle barStyle;
- private final boolean smooth;
-
- /**
- *
- * @param barColor Color of BossBar
- * @param barStyle Style of BossBar
- * @param smooth true Refresh every tick else every second
- */
- public BossBarDisplay(String title, BarColor barColor, BarStyle barStyle, boolean smooth) {
- this.barColor = barColor;
- this.barStyle = barStyle;
- this.smooth = smooth;
-
- this.worldEventBossBar = new WorldEventBossBar(ChatUtil.color(title), barColor, barStyle, smooth);
- }
-
- @Override
- public void startDisplay(WorldEvent event) {
- event.setWorldEventBossBar(getWorldEventBossBar());
-
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (WorldEvent.getAffectedPlayers().contains(player)) {
- event.getWorldEventBossBar().getBossBar().addPlayer(player);
- }
- }
- }
-
- @Override
- public void updateDisplay(WorldEvent event, double progress) {
- if (event.getWorldEventBossBar() != null && event.getWorldEventBossBar().getBossBar() != null) {
- event.getWorldEventBossBar().getBossBar().setProgress(progress);
- }
- }
-
- @Override
- public void stopDisplay(WorldEvent event) {
- if (event.getWorldEventBossBar() != null && event.getWorldEventBossBar().getBossBar() != null) {
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (WorldEvent.getAffectedPlayers().contains(player)) {
- event.getWorldEventBossBar().getBossBar().removePlayer(player);
- }
- }
- }
- }
-
- public WorldEventBossBar getWorldEventBossBar() {
- return worldEventBossBar;
- }
-
- public BarColor getBarColor() {
- return barColor;
- }
-
- public BarStyle getBarStyle() {
- return barStyle;
- }
-
- public boolean isSmooth() {
- return smooth;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/WorldEventBossBar.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/WorldEventBossBar.java
deleted file mode 100644
index 429cdfb..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/bossbar/WorldEventBossBar.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display.bossbar;
-
-import org.bukkit.Bukkit;
-import org.bukkit.boss.BarColor;
-import org.bukkit.boss.BarStyle;
-import org.bukkit.boss.BossBar;
-
-public class WorldEventBossBar {
- private String title;
- private BarColor barColor;
- private BarStyle barStyle;
- private boolean smooth;
- private BossBar bossBar;
-
- public WorldEventBossBar(String title, BarColor barColor, BarStyle barStyle, boolean smooth) {
- this.title = title;
- this.barColor = barColor;
- this.barStyle = barStyle;
- this.smooth = smooth;
-
- this.bossBar = Bukkit.createBossBar(this.title, this.barColor, this.barStyle);
- }
-
- public String getTitle() {
- return title;
- }
-
- public BarColor getBarColor() {
- return barColor;
- }
-
- public BarStyle getBarStyle() {
- return barStyle;
- }
-
- public boolean isSmooth() {
- return smooth;
- }
-
- public BossBar getBossBar() {
- return bossBar;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public void setBarColor(BarColor barColor) {
- this.barColor = barColor;
- }
-
- public void setBarStyle(BarStyle barStyle) {
- this.barStyle = barStyle;
- }
-
- public void setSmooth(boolean smooth) {
- this.smooth = smooth;
- }
-
- public void setBossBar(BossBar bossBar) {
- this.bossBar = bossBar;
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/chat/ChatDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/chat/ChatDisplay.java
deleted file mode 100644
index 212c2b4..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/chat/ChatDisplay.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display.chat;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.util.display.IWorldEventDisplay;
-import com.projectkorra.rpg.util.ChatUtil;
-import org.bukkit.Bukkit;
-import org.bukkit.entity.Player;
-
-public class ChatDisplay implements IWorldEventDisplay {
- private final String startMessage;
- private final String stopMessage;
-
- public ChatDisplay(String startMessage, String stopMessage) {
- this.startMessage = startMessage;
- this.stopMessage = stopMessage;
- }
-
- @Override
- public void startDisplay(WorldEvent event) {
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (WorldEvent.getAffectedPlayers().contains(player)) {
- ChatUtil.sendBrandingMessage(player, this.startMessage);
- }
- }
- }
-
- @Override
- public void updateDisplay(WorldEvent event, double progress) {}
-
- @Override
- public void stopDisplay(WorldEvent event) {
- for (Player player : Bukkit.getOnlinePlayers()) {
- if (WorldEvent.getAffectedPlayers().contains(player)) {
- ChatUtil.sendBrandingMessage(player, this.stopMessage);
- }
- }
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/none/NoDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/none/NoDisplay.java
deleted file mode 100644
index 1d7b716..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/none/NoDisplay.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display.none;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.util.display.IWorldEventDisplay;
-
-public class NoDisplay implements IWorldEventDisplay {
- @Override
- public void startDisplay(WorldEvent event) {
- // MAYBE LOG
- }
-
- @Override
- public void updateDisplay(WorldEvent event, double progress) {
- // MAYBE LOG
- }
-
- @Override
- public void stopDisplay(WorldEvent event) {
- // MAYBE LOG
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/scoreboard/ScoreboardDisplay.java b/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/scoreboard/ScoreboardDisplay.java
deleted file mode 100644
index 4e68686..0000000
--- a/src/main/java/com/projectkorra/rpg/modules/worldevents/util/display/scoreboard/ScoreboardDisplay.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.projectkorra.rpg.modules.worldevents.util.display.scoreboard;
-
-import com.projectkorra.rpg.modules.worldevents.WorldEvent;
-import com.projectkorra.rpg.modules.worldevents.util.display.IWorldEventDisplay;
-
-public class ScoreboardDisplay implements IWorldEventDisplay {
- @Override
- public void startDisplay(WorldEvent event) {
-
- }
-
- @Override
- public void updateDisplay(WorldEvent event, double progress) {
-
- }
-
- @Override
- public void stopDisplay(WorldEvent event) {
-
- }
-}
diff --git a/src/main/java/com/projectkorra/rpg/storage/TableCreator.java b/src/main/java/com/projectkorra/rpg/storage/TableCreator.java
index 835f5fc..fe62fd3 100644
--- a/src/main/java/com/projectkorra/rpg/storage/TableCreator.java
+++ b/src/main/java/com/projectkorra/rpg/storage/TableCreator.java
@@ -1,5 +1,6 @@
package com.projectkorra.rpg.storage;
+import com.projectkorra.projectkorra.ProjectKorra;
import com.projectkorra.projectkorra.storage.DBConnection;
import com.projectkorra.projectkorra.storage.MySQL;
import com.projectkorra.rpg.ProjectKorraRPG;
@@ -8,7 +9,7 @@ public class TableCreator extends DBConnection {
public static final String RPG_PLAYER_TABLE = "pkrpg_players";
public static final String RPG_SCHEDULE_TABLE = "pkrpg_schedule";
public static final String RPG_AVATAR_TABLE = "pkrpg_avatars";
- public static final String RPG_PASTLIVES_TABLE = "pkrpg_pastlives";
+ public static final String RPG_PAST_LIVES_TABLE = "pkrpg_pastlives";
public TableCreator() {
this.createRpgPlayerTable();
@@ -80,13 +81,13 @@ private void createScheduleTable() {
private void createRpgAvatarTable() {
if (sql instanceof MySQL) {
if (!sql.tableExists(RPG_AVATAR_TABLE)) {
- ProjectKorraRPG.getPlugin().getLogger().info("Creating " + RPG_AVATAR_TABLE + " table");
+ ProjectKorra.log.info("Creating " + RPG_AVATAR_TABLE + " table");
final String query = "CREATE TABLE `" + RPG_AVATAR_TABLE + "` ("
+ "`uuid` varchar(36) NOT NULL,"
- + "`main_element` varchar(255) NOT NULL,"
- + "`sub_elements` varchar(255) NOT NULL,"
- + "`chosen_time` datetime NOT NULL,"
+ + "`player` varchar(255) NOT NULL,"
+ + "`startTime" + "` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ + "`elements` varchar(255) NOT NULL,"
+ "PRIMARY KEY (`uuid`)"
+ ");";
@@ -94,14 +95,13 @@ private void createRpgAvatarTable() {
}
} else {
if (!sql.tableExists(RPG_AVATAR_TABLE)) {
- ProjectKorraRPG.getPlugin().getLogger().info("Creating " + RPG_AVATAR_TABLE + " table");
+ ProjectKorra.log.info("Creating " + RPG_AVATAR_TABLE + " table");
- final String query = "CREATE TABLE " + RPG_AVATAR_TABLE + " ("
- + "uuid TEXT NOT NULL, "
- + "main_element TEXT NOT NULL, "
- + "sub_elements TEXT NOT NULL, "
- + "chosen_time TIMESTAMP NOT NULL, "
- + "PRIMARY KEY (uuid)"
+ final String query = "CREATE TABLE `" + RPG_AVATAR_TABLE + "` ("
+ + "`uuid` TEXT(36) PRIMARY KEY,"
+ + "`player` TEXT(16) NOT NULL,"
+ + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ + "`elements` TEXT(255) NOT NULL"
+ ");";
sql.modifyQuery(query, false);
@@ -111,104 +111,37 @@ private void createRpgAvatarTable() {
private void createRpgPastLivesTable() {
if (sql instanceof MySQL) {
- if (!sql.tableExists(RPG_PASTLIVES_TABLE)) {
- ProjectKorraRPG.getPlugin().getLogger().info("Creating " + RPG_PASTLIVES_TABLE + " table");
+ if (!sql.tableExists(RPG_PAST_LIVES_TABLE)) {
+ ProjectKorra.log.info("Creating " + RPG_PAST_LIVES_TABLE + " table");
- final String query = "CREATE TABLE `" + RPG_PASTLIVES_TABLE + "` ("
+ final String query = "CREATE TABLE `" + RPG_PAST_LIVES_TABLE + "` ("
+ "`uuid` varchar(36) NOT NULL,"
- + "`main_element` varchar(255) NOT NULL,"
- + "`sub_elements` varchar(255) NOT NULL,"
- + "`chosen_time` datetime NOT NULL,"
- + "`end_time` datetime NOT NULL,"
- + "`end_reason` varchar(255) DEFAULT NULL,"
- + "PRIMARY KEY (`uuid`)"
+ + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ + "`player` varchar(255) NOT NULL,"
+ + "`endTime` datetime DEFAULT NULL,"
+ + "`elements` varchar(255) NOT NULL,"
+ + "`endReason` varchar(255) DEFAULT NULL,"
+ + "PRIMARY KEY (`uuid`, `startTime`)"
+ ");";
sql.modifyQuery(query, false);
}
} else {
- if (!sql.tableExists(RPG_PASTLIVES_TABLE)) {
- ProjectKorraRPG.getPlugin().getLogger().info("Creating " + RPG_PASTLIVES_TABLE + " table");
-
- final String query = "CREATE TABLE " + RPG_PASTLIVES_TABLE + " ("
- + "uuid TEXT NOT NULL, "
- + "main_element TEXT NOT NULL, "
- + "sub_elements TEXT NOT NULL, "
- + "chosenTime TIMESTAMP NOT NULL, "
- + "endTime TIMESTAMP NOT NULL, "
- + "endReason TEXT DEFAULT NULL, "
- + "PRIMARY KEY (uuid)"
+ if (!sql.tableExists(RPG_PAST_LIVES_TABLE)) {
+ ProjectKorra.log.info("Creating " + RPG_PAST_LIVES_TABLE + " table");
+
+ final String query = "CREATE TABLE `" + RPG_PAST_LIVES_TABLE + "` ("
+ + "`uuid` TEXT(36) NOT NULL,"
+ + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
+ + "`player` TEXT(16) NOT NULL,"
+ + "`endTime` datetime DEFAULT NULL,"
+ + "`elements` TEXT(255) NOT NULL,"
+ + "`endReason` TEXT(255) DEFAULT NULL,"
+ + "PRIMARY KEY (`uuid`, `startTime`)"
+ ");";
sql.modifyQuery(query, false);
}
}
}
-
-// private void createRpgAvatarTable() {
-// if (sql instanceof MySQL) {
-// if (!sql.tableExists(RPG_AVATAR_TABLE)) {
-// ProjectKorra.log.info("Creating " + RPG_AVATAR_TABLE + " table");
-//
-// final String query = "CREATE TABLE `" + RPG_AVATAR_TABLE + "` ("
-// + "`uuid` varchar(36) NOT NULL,"
-// + "`player` varchar(255) NOT NULL,"
-// + "`startTime" + "` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
-// + "`elements` varchar(255) NOT NULL,"
-// + "PRIMARY KEY (`uuid`)"
-// + ");";
-//
-// sql.modifyQuery(query, false);
-// }
-// } else {
-// if (!sql.tableExists(RPG_AVATAR_TABLE)) {
-// ProjectKorra.log.info("Creating " + RPG_AVATAR_TABLE + " table");
-//
-// final String query = "CREATE TABLE `" + RPG_AVATAR_TABLE + "` ("
-// + "`uuid` TEXT(36) PRIMARY KEY,"
-// + "`player` TEXT(16) NOT NULL,"
-// + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
-// + "`elements` TEXT(255) NOT NULL"
-// + ");";
-//
-// sql.modifyQuery(query, false);
-// }
-// }
-// }
-//
-// private void createRpgPastLivesTable() {
-// if (sql instanceof MySQL) {
-// if (!sql.tableExists(RPG_PASTLIVES_TABLE)) {
-// ProjectKorra.log.info("Creating " + RPG_PASTLIVES_TABLE + " table");
-//
-// final String query = "CREATE TABLE `" + RPG_PASTLIVES_TABLE + "` ("
-// + "`uuid` varchar(36) NOT NULL,"
-// + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
-// + "`player` varchar(255) NOT NULL,"
-// + "`endTime` datetime DEFAULT NULL,"
-// + "`elements` varchar(255) NOT NULL,"
-// + "`endReason` varchar(255) DEFAULT NULL,"
-// + "PRIMARY KEY (`uuid`, `startTime`)"
-// + ");";
-//
-// sql.modifyQuery(query, false);
-// }
-// } else {
-// if (!sql.tableExists(RPG_PASTLIVES_TABLE)) {
-// ProjectKorra.log.info("Creating " + RPG_PASTLIVES_TABLE + " table");
-//
-// final String query = "CREATE TABLE `" + RPG_PASTLIVES_TABLE + "` ("
-// + "`uuid` TEXT(36) NOT NULL,"
-// + "`startTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,"
-// + "`player` TEXT(16) NOT NULL,"
-// + "`endTime` datetime DEFAULT NULL,"
-// + "`elements` TEXT(255) NOT NULL,"
-// + "`endReason` TEXT(255) DEFAULT NULL,"
-// + "PRIMARY KEY (`uuid`, `startTime`)"
-// + ");";
-//
-// sql.modifyQuery(query, false);
-// }
-// }
-// }
}
diff --git a/src/main/java/com/projectkorra/rpg/ui/InventoryUI.java b/src/main/java/com/projectkorra/rpg/ui/InventoryUI.java
new file mode 100644
index 0000000..835e235
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/InventoryUI.java
@@ -0,0 +1,18 @@
+package com.projectkorra.rpg.ui;
+
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryHolder;
+import org.jetbrains.annotations.NotNull;
+
+public interface InventoryUI extends InventoryHolder {
+ void open(final @NotNull Player player);
+
+ default void handleClick(final InventoryClickEvent event) {}
+
+ // Close callback
+ default void onClose(final @NotNull Player player) {}
+
+ @Override @NotNull Inventory getInventory();
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/Slot.java b/src/main/java/com/projectkorra/rpg/ui/Slot.java
new file mode 100644
index 0000000..9dc95aa
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/Slot.java
@@ -0,0 +1,12 @@
+package com.projectkorra.rpg.ui;
+
+public record Slot(int x, int y) {
+ public static final int COLUMNS = 9;
+ public int index() {
+ return y * COLUMNS + x;
+ }
+
+ public static Slot of(int x, int y) {
+ return new Slot(x, y);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/builder/InventoryUIBuilder.java b/src/main/java/com/projectkorra/rpg/ui/builder/InventoryUIBuilder.java
new file mode 100644
index 0000000..96f5395
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/builder/InventoryUIBuilder.java
@@ -0,0 +1,107 @@
+package com.projectkorra.rpg.ui.builder;
+
+import com.projectkorra.projectkorra.util.ChatUtil;
+import com.projectkorra.rpg.ui.InventoryUI;
+import com.projectkorra.rpg.ui.Slot;
+import com.projectkorra.rpg.ui.impl.BasicInventoryUI;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.ItemStack;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class InventoryUIBuilder {
+ private static final int MIN_ROWS = 1;
+ private static final int MAX_ROWS = 6;
+ private static final int COLUMNS = Slot.COLUMNS; // 9
+
+ private final Map items = new HashMap<>();
+ private final Map> clickHandlers = new HashMap<>();
+
+ private final int rows;
+ private final String title;
+
+ private InventoryUIBuilder(int rows, String title) {
+ this.rows = rows;
+ this.title = title;
+ }
+
+ public static InventoryUIBuilder create(int rows, String title) {
+ if (rows < MIN_ROWS || rows > MAX_ROWS) {
+ throw new IllegalArgumentException("rows must be between " + MIN_ROWS + "-" + MAX_ROWS + "!");
+ }
+ return new InventoryUIBuilder(rows, ChatUtil.color(title));
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public InventoryUIBuilder withItem(int x, int y, ItemStack item) {
+ validateXY(x, y);
+ items.put(Slot.of(x, y), item);
+ return this;
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ public InventoryUIBuilder withButton(int x, int y, ItemStack item, Consumer onClick) {
+ withItem(x, y, item);
+ clickHandlers.put(Slot.of(x, y), onClick);
+ return this;
+ }
+
+ private void validateXY(int x, int y) {
+ if (x < 0 || x >= getWidth() || y < 0 || y >= getHeight()) {
+ throw new IllegalArgumentException("Slot out of bounds! x=" + x + " (0-" + (getWidth()-1) + "), y=" + y + " (0-" + (getHeight()-1) + ")");
+ }
+ }
+
+ public InventoryUIBuilder fill(ItemStack filler) {
+ for (int y = 0; y < getHeight(); y++) {
+ for (int x = 0; x < getWidth(); x++) {
+ withItem(x, y, filler);
+ }
+ }
+ return this;
+ }
+
+ public InventoryUIBuilder fillLeftRight(ItemStack border, ItemStack fill) {
+ for (int y = 0; y < getHeight(); y++) {
+ for (int x = 0; x < getWidth(); x++) {
+ if (x == 0 || x == 8) {
+ withItem(x, y, border);
+ continue;
+ }
+ withItem(x, y, fill);
+ }
+ }
+ return this;
+ }
+
+ public InventoryUIBuilder fillBorder(ItemStack borderItem) {
+ int w = getWidth();
+ int h = getHeight();
+
+ for (int x = 0; x < w; x++) {
+ withItem(x, 0, borderItem);
+ withItem(x, h-1, borderItem);
+ }
+
+ for (int y = 1; y < h; y++) {
+ withItem(0, y, borderItem);
+ withItem(w - 1, y, borderItem);
+ }
+
+ return this;
+ }
+
+ public int getWidth() {
+ return COLUMNS;
+ }
+
+ public int getHeight() {
+ return rows;
+ }
+
+ public InventoryUI build() {
+ return new BasicInventoryUI(rows, title, items, clickHandlers);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/impl/BasicInventoryUI.java b/src/main/java/com/projectkorra/rpg/ui/impl/BasicInventoryUI.java
new file mode 100644
index 0000000..fc5d365
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/impl/BasicInventoryUI.java
@@ -0,0 +1,43 @@
+package com.projectkorra.rpg.ui.impl;
+
+import com.projectkorra.rpg.ui.InventoryUI;
+import com.projectkorra.rpg.ui.Slot;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.ItemStack;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+public class BasicInventoryUI implements InventoryUI {
+ private final Map> clickHandlers;
+ private final Inventory inventory;
+
+ public BasicInventoryUI(int rows, String title, Map items, Map> clickHandlers) {
+ this.clickHandlers = Map.copyOf(clickHandlers);
+ this.inventory = Bukkit.createInventory(this, rows * 9, title);
+
+ items.forEach((slot, stack) -> inventory.setItem(slot.index(), stack));
+ }
+
+ @Override
+ public void open(@NotNull Player player) {
+ player.openInventory(inventory);
+ }
+
+ @Override
+ public @NotNull Inventory getInventory() {
+ return inventory;
+ }
+
+ @Override
+ public void handleClick(InventoryClickEvent event) {
+ Slot slot = Slot.of(event.getRawSlot() % 9, event.getRawSlot() / 9);
+ Consumer action = clickHandlers.get(slot);
+ if (action != null) action.accept(event);
+ event.setCancelled(true);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/menu/Menu.java b/src/main/java/com/projectkorra/rpg/ui/menu/Menu.java
new file mode 100644
index 0000000..b89452a
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/menu/Menu.java
@@ -0,0 +1,14 @@
+package com.projectkorra.rpg.ui.menu;
+
+import com.projectkorra.rpg.ProjectKorraRPG;
+import com.projectkorra.rpg.ui.InventoryUI;
+import org.bukkit.entity.Player;
+
+public interface Menu {
+ InventoryUI buildUI(Player player);
+
+ default void open(Player player) {
+ InventoryUI ui = buildUI(player);
+ ProjectKorraRPG.getPlugin().getInventoryService().open(player, ui);
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/service/InventoryEventListener.java b/src/main/java/com/projectkorra/rpg/ui/service/InventoryEventListener.java
new file mode 100644
index 0000000..efc5daf
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/service/InventoryEventListener.java
@@ -0,0 +1,42 @@
+package com.projectkorra.rpg.ui.service;
+
+import com.projectkorra.rpg.ui.InventoryUI;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+
+/**
+ * Handle onClose and onClick to clean up the UI and Memory properly
+ */
+public class InventoryEventListener implements Listener {
+ private final InventoryService service;
+
+ public InventoryEventListener(InventoryService service) {
+ this.service = service;
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ public void onClick(InventoryClickEvent event) {
+ if (!((event.getWhoClicked()) instanceof Player player)) return;
+
+ InventoryUI ui = service.get(player);
+
+ if (ui != null && event.getInventory().getHolder() == ui) {
+ event.setCancelled(true);
+ ui.handleClick(event);
+ }
+ }
+
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ public void onInventoryClose(InventoryCloseEvent event) {
+ if (!(event.getPlayer() instanceof Player player)) return;
+ InventoryUI ui = service.get(player);
+
+ if (ui != null && event.getInventory().getHolder() == ui) {
+ service.close(player);
+ }
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/service/InventoryService.java b/src/main/java/com/projectkorra/rpg/ui/service/InventoryService.java
new file mode 100644
index 0000000..d5f362b
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/service/InventoryService.java
@@ -0,0 +1,45 @@
+package com.projectkorra.rpg.ui.service;
+
+import com.projectkorra.rpg.ui.InventoryUI;
+import org.bukkit.entity.Player;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Keeps track of open UIs and provides open / close methods.
+ */
+public class InventoryService {
+ private final Map openUis = new HashMap<>();
+
+ public void open(Player player, InventoryUI ui) {
+ InventoryUI old = openUis.put(player, ui);
+
+ // Close any previously open menus
+ if (old != null && old != ui) {
+ old.onClose(player);
+ }
+
+ ui.open(player);
+ }
+
+ public InventoryUI get(Player player) {
+ return openUis.get(player);
+ }
+
+ public void close(Player player) {
+ InventoryUI old = openUis.remove(player);
+ if (old != null) {
+ old.onClose(player);
+ }
+ }
+
+ public void closeAll() {
+ openUis.forEach((p, ui) -> ui.onClose(p));
+ openUis.clear();
+ }
+
+ public Map getOpenUis() {
+ return openUis;
+ }
+}
diff --git a/src/main/java/com/projectkorra/rpg/ui/util/ItemUtil.java b/src/main/java/com/projectkorra/rpg/ui/util/ItemUtil.java
new file mode 100644
index 0000000..ea82d11
--- /dev/null
+++ b/src/main/java/com/projectkorra/rpg/ui/util/ItemUtil.java
@@ -0,0 +1,31 @@
+package com.projectkorra.rpg.ui.util;
+
+import com.projectkorra.projectkorra.util.ChatUtil;
+import org.bukkit.Material;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+
+import java.util.List;
+import java.util.Objects;
+
+public final class ItemUtil {
+ private ItemUtil() {}
+
+ public static ItemStack create(Material material, String name) {
+ return create(material, name, List.of());
+ }
+
+ public static ItemStack create(Material material, String name, List lore) {
+ ItemStack item = new ItemStack(material);
+ ItemMeta meta = Objects.requireNonNull(item.getItemMeta(), () -> "Cannot get ItemMeta for " + material);
+ meta.setDisplayName(ChatUtil.color(name));
+
+ if (!lore.isEmpty()) {
+ List coloredLore = lore.stream().map(ChatUtil::color).toList();
+ meta.setLore(coloredLore);
+ }
+
+ item.setItemMeta(meta);
+ return item;
+ }
+}