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; + } +}