diff --git a/README.md b/README.md index c503960..32a3b66 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,15 @@ | `/sharedgroup leaderboard` | Show the group's death leaderboard during a game | Group member | | `/sharedgroup enable ` | Enable synchronization for a stat, outside a game | OP | | `/sharedgroup disable ` | Disable synchronization for a stat, outside a game | OP | +| `/sharedgroup configuration` | Open the synchronization settings menu, outside a game | OP, group owner | | `/sharedgroup version` | Display the plugin version | None | +## Food synchronization + +The food level is shared across the group, but saturation is not: each player keeps their own saturation. This is a deliberate choice. Every player's hunger drains the shared food bar, so food goes down much faster than in solo play. + ## What remains to be done -- Synchronize players' food levels and saturation. - Add automated tests for the group lifecycle and stat synchronization. Made for Minecraft 26.2 with PaperMC. diff --git a/src/main/java/fr/korastrip/paperplugin/SharedStats.java b/src/main/java/fr/korastrip/paperplugin/SharedStats.java index 7ea87e7..4bbaffe 100644 --- a/src/main/java/fr/korastrip/paperplugin/SharedStats.java +++ b/src/main/java/fr/korastrip/paperplugin/SharedStats.java @@ -32,6 +32,9 @@ public void onEnable() { this.getServer().getPluginManager().registerEvents(new RegenerationListener(this), this); this.getServer().getPluginManager().registerEvents(new PlayerRespawnListener(this), this); this.getServer().getPluginManager().registerEvents(new EntityResurrectListener(this), this); + this.getServer().getPluginManager().registerEvents(new FoodListener(this), this); + this.getServer().getPluginManager().registerEvents(new InventoryClickListener(this), this); + this.getServer().getPluginManager().registerEvents(new InventoryDragListener(), this); this.getLifecycleManager().registerEventHandler( LifecycleEvents.COMMANDS, diff --git a/src/main/java/fr/korastrip/paperplugin/SynchronizationConfig.java b/src/main/java/fr/korastrip/paperplugin/SynchronizationConfig.java index 0f98c05..52398cb 100644 --- a/src/main/java/fr/korastrip/paperplugin/SynchronizationConfig.java +++ b/src/main/java/fr/korastrip/paperplugin/SynchronizationConfig.java @@ -63,6 +63,10 @@ public boolean setEnabled(String stat, boolean enabled) { } } + public boolean toggle(String stat) { + return setEnabled(stat, !isEnabled(stat)); + } + private String getKey(String stat) { String normalizedStat = stat.toLowerCase(Locale.ROOT); diff --git a/src/main/java/fr/korastrip/paperplugin/commands/group/GroupCommand.java b/src/main/java/fr/korastrip/paperplugin/commands/group/GroupCommand.java index 70c97ca..bf67599 100644 --- a/src/main/java/fr/korastrip/paperplugin/commands/group/GroupCommand.java +++ b/src/main/java/fr/korastrip/paperplugin/commands/group/GroupCommand.java @@ -28,6 +28,7 @@ public LiteralCommandNode create() { .then(new Stats(plugin).build()) .then(new Enable(plugin).build()) .then(new Disable(plugin).build()) + .then(new Configuration(plugin).build()) .build(); } diff --git a/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Configuration.java b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Configuration.java new file mode 100644 index 0000000..db8d2db --- /dev/null +++ b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Configuration.java @@ -0,0 +1,44 @@ +package fr.korastrip.paperplugin.commands.group.sub; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import fr.korastrip.paperplugin.SharedStats; +import fr.korastrip.paperplugin.commands.group.GroupSubCommand; +import fr.korastrip.paperplugin.enums.ErrorMessage; +import fr.korastrip.paperplugin.inventory.ConfigurationInventory; +import fr.korastrip.paperplugin.model.SharedGroup; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.bukkit.entity.Player; + +public class Configuration extends GroupSubCommand { + + public Configuration(SharedStats plugin) { + super(plugin, "configuration", "Configure SharedStats using an interface.", ""); + } + + @Override + public LiteralArgumentBuilder build() { + return Commands.literal("configuration").executes(context -> { + ContextResult ctx = this.getContext(context); + SharedGroup group = this.plugin.getGroup(); + + if (!(ctx.entity() instanceof Player player)) { + return this.generateError("This command cannot be used by the console :)", ctx.entity()); + } + + if(!player.getUniqueId().equals(group.getOwnerUuid())) { + return this.generateError(ErrorMessage.NOT_OWNER, player); + } + + if(group.getIsPlaying()) { + return this.generateError(ErrorMessage.GAME_RUNNING, player); + } + + ConfigurationInventory configurationInventory = new ConfigurationInventory(this.plugin); + player.openInventory(configurationInventory.getInventory()); + + return Command.SINGLE_SUCCESS; + }).requires(source -> source.getSender().isOp()); + } +} diff --git a/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Remove.java b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Remove.java index 5be8682..80073a8 100644 --- a/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Remove.java +++ b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Remove.java @@ -38,7 +38,7 @@ public LiteralArgumentBuilder build() { return this.generateError(ErrorMessage.NO_GROUP, ctx.sender()); } - if (!group.getOwnerUuid().equals(player.getUniqueId())) { + if (!player.getUniqueId().equals(group.getOwnerUuid())) { return this.generateError(ErrorMessage.NOT_OWNER, ctx.sender()); } diff --git a/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Start.java b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Start.java index b37522e..277e9d4 100644 --- a/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Start.java +++ b/src/main/java/fr/korastrip/paperplugin/commands/group/sub/Start.java @@ -49,6 +49,9 @@ public LiteralArgumentBuilder build() { group.setHealth(20.0); group.syncHealth(); + group.setFood(20); + group.syncFood(); + players.forEach(p -> { p.playSound( @@ -58,6 +61,8 @@ public LiteralArgumentBuilder build() { 1f ); + p.setSaturation(20.0f); + p.sendRichMessage("The game has started. Do not die!"); }); diff --git a/src/main/java/fr/korastrip/paperplugin/inventory/ConfigurationInventory.java b/src/main/java/fr/korastrip/paperplugin/inventory/ConfigurationInventory.java new file mode 100644 index 0000000..897ce3d --- /dev/null +++ b/src/main/java/fr/korastrip/paperplugin/inventory/ConfigurationInventory.java @@ -0,0 +1,106 @@ +package fr.korastrip.paperplugin.inventory; + +import fr.korastrip.paperplugin.SharedStats; +import io.papermc.paper.datacomponent.DataComponentTypes; +import io.papermc.paper.datacomponent.item.TooltipDisplay; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import org.bukkit.Material; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +public class ConfigurationInventory implements InventoryHolder { + + private final Inventory inventory; + private final SharedStats plugin; + + public ConfigurationInventory(SharedStats plugin) { + this.inventory = plugin.getServer().createInventory(this, 9 * 5, Component.text("Configuration")); + this.plugin = plugin; + + ItemStack whiteGlassPane = this.empty(new ItemStack(Material.WHITE_STAINED_GLASS_PANE)); + ItemStack blackGlassPane = this.empty(new ItemStack(Material.BLACK_STAINED_GLASS_PANE)); + + ItemStack toggleFoodConfiguration = this.renameItemStack(new ItemStack(Material.COOKED_BEEF), this.getDisplayName("food")); + ItemStack toggleHealthConfiguration = this.renameItemStack(new ItemStack(Material.GLISTERING_MELON_SLICE), this.getDisplayName("health")); + ItemStack togglePotionConfiguration = this.renameItemStack(new ItemStack(Material.POTION), this.getDisplayName("potions")); + + togglePotionConfiguration.setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().addHiddenComponents(DataComponentTypes.POTION_CONTENTS).build()); + + this.inventory.setItem(20, toggleHealthConfiguration); + this.inventory.setItem(22, toggleFoodConfiguration); + this.inventory.setItem(24, togglePotionConfiguration); + + for (int i = 0; i < 45; i++) { + if(i >= 9 && i < 36) continue; + this.inventory.setItem(i, whiteGlassPane); + } + + for(int i = 9; i < 36; i++) { + if(i == 20 || i == 22 || i == 24) continue; + this.inventory.setItem(i, blackGlassPane); + } + } + + @Override + public Inventory getInventory() { + return this.inventory; + } + + private ItemStack renameItemStack(ItemStack item, String name) { + ItemMeta meta = item.getItemMeta(); + meta.displayName( + MiniMessage.miniMessage().deserialize(name) + ); + item.setItemMeta(meta); + return item; + } + + private ItemStack empty(ItemStack itemStack) { + ItemMeta meta = itemStack.getItemMeta(); + meta.displayName(Component.empty()); + itemStack.setItemMeta(meta); + return itemStack; + } + + private String isEnabled(String type) { + return this.plugin.getSynchronizationConfig().isEnabled(type) ? "Enabled" : "Disabled"; + } + + private String getDisplayName(String stat) { + String label = switch (stat) { + case "health" -> "Health"; + case "food" -> "Food"; + case "potions" -> "Potions"; + default -> stat; + }; + + return "" + label + ": " + this.isEnabled(stat); + } + + public void refresh(String stat) { + + int slot = switch (stat) { + case "health" -> 20; + case "food" -> 22; + case "potions" -> 24; + default -> -1; + }; + + if (slot == -1) return; + ItemStack item = this.inventory.getItem(slot); + if (item == null) return; + + + item.editMeta(meta -> { + meta.displayName( + MiniMessage.miniMessage().deserialize(this.getDisplayName(stat)) + ); + }); + + this.inventory.setItem(slot, item); + } + +} diff --git a/src/main/java/fr/korastrip/paperplugin/listener/FoodListener.java b/src/main/java/fr/korastrip/paperplugin/listener/FoodListener.java new file mode 100644 index 0000000..898f957 --- /dev/null +++ b/src/main/java/fr/korastrip/paperplugin/listener/FoodListener.java @@ -0,0 +1,46 @@ +package fr.korastrip.paperplugin.listener; + +import fr.korastrip.paperplugin.SharedStats; +import fr.korastrip.paperplugin.model.SharedGroup; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.FoodLevelChangeEvent; + +public class FoodListener implements Listener { + + private final SharedStats plugin; + + public FoodListener(SharedStats plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onFoodLevelChange(FoodLevelChangeEvent e) { + if (e.getEntity() instanceof Player player) { + SharedGroup group = this.plugin.getGroup(); + + if(!this.plugin.getSynchronizationConfig().isEnabled("food")) return; + if(!group.getPlayersId().contains(player.getUniqueId())) return; + if(!group.getIsPlaying()) return; + + if (e.getFoodLevel() == group.getFood()) { + e.setCancelled(true); + return; + } + + if (player.getFoodLevel() == group.getFood()) { + int newFood = Math.clamp(e.getFoodLevel(), 0, 20); + + group.setFood(newFood); + group.syncFood(); + + e.setCancelled(true); + } else { + group.syncFood(); + e.setCancelled(true); + } + } + } + +} diff --git a/src/main/java/fr/korastrip/paperplugin/listener/InventoryClickListener.java b/src/main/java/fr/korastrip/paperplugin/listener/InventoryClickListener.java new file mode 100644 index 0000000..8ce63cb --- /dev/null +++ b/src/main/java/fr/korastrip/paperplugin/listener/InventoryClickListener.java @@ -0,0 +1,67 @@ +package fr.korastrip.paperplugin.listener; + +import fr.korastrip.paperplugin.SharedStats; +import fr.korastrip.paperplugin.enums.ErrorMessage; +import fr.korastrip.paperplugin.inventory.ConfigurationInventory; +import fr.korastrip.paperplugin.model.SharedGroup; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +public class InventoryClickListener implements Listener { + + private final SharedStats plugin; + + public InventoryClickListener(SharedStats plugin) { + this.plugin = plugin; + } + + @EventHandler + public void onInventoryClickListener(InventoryClickEvent e) { + Inventory inventory = e.getInventory(); + SharedGroup group = this.plugin.getGroup(); + + if (!(inventory.getHolder(false) instanceof ConfigurationInventory configuration)) { + return; + } + + e.setCancelled(true); + + if (group.getIsPlaying()) { + return; + } + + ItemStack clickedItem = e.getCurrentItem(); + if (clickedItem == null) return; + + int slot = e.getRawSlot(); + + String stats; + + switch (slot) { + case 20 -> { + stats = "health"; + } + case 22 -> { + stats = "food"; + } + case 24 -> { + stats = "potions"; + } + default -> { + stats = null; + } + } + + if (stats == null) return; + + if (!this.plugin.getSynchronizationConfig().toggle(stats)) { + e.getWhoClicked().sendRichMessage("" + ErrorMessage.CONFIG_SAVE_FAILED.getMessage() + ""); + return; + } + + configuration.refresh(stats); + } +} diff --git a/src/main/java/fr/korastrip/paperplugin/listener/InventoryDragListener.java b/src/main/java/fr/korastrip/paperplugin/listener/InventoryDragListener.java new file mode 100644 index 0000000..550b018 --- /dev/null +++ b/src/main/java/fr/korastrip/paperplugin/listener/InventoryDragListener.java @@ -0,0 +1,22 @@ +package fr.korastrip.paperplugin.listener; + +import fr.korastrip.paperplugin.inventory.ConfigurationInventory; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; + +public class InventoryDragListener implements Listener { + + @EventHandler + public void onInventoryDrag(InventoryDragEvent e) { + Inventory inventory = e.getInventory(); + + if (!(inventory.getHolder(false) instanceof ConfigurationInventory)) { + return; + } + + e.setCancelled(true); + } + +} diff --git a/src/main/java/fr/korastrip/paperplugin/listener/PlayerRespawnListener.java b/src/main/java/fr/korastrip/paperplugin/listener/PlayerRespawnListener.java index 1595c64..14399a7 100644 --- a/src/main/java/fr/korastrip/paperplugin/listener/PlayerRespawnListener.java +++ b/src/main/java/fr/korastrip/paperplugin/listener/PlayerRespawnListener.java @@ -32,6 +32,10 @@ public void onPlayerRespawn(PlayerRespawnEvent event) { group.setHealth(20); group.syncHealth(); } + if(plugin.getSynchronizationConfig().isEnabled("food")) { + group.setFood(20); + group.syncFood(); + } group.clearRespawnedPlayers(); group.isGroupDying = false; } diff --git a/src/main/java/fr/korastrip/paperplugin/model/SharedGroup.java b/src/main/java/fr/korastrip/paperplugin/model/SharedGroup.java index 95b3773..acb95a5 100644 --- a/src/main/java/fr/korastrip/paperplugin/model/SharedGroup.java +++ b/src/main/java/fr/korastrip/paperplugin/model/SharedGroup.java @@ -43,7 +43,14 @@ public double getHealth() { } - private final double groupFood = 20.0; + private int groupFood = 20; + public void setFood(int food) { + this.groupFood = food; + } + public int getFood() { + return this.groupFood; + } + private int deadCount = 0; public int getDeadCount() { @@ -116,6 +123,10 @@ public void syncHealth() { this.getOnlinePlayers().stream().filter(p -> !p.isDead()).forEach(p -> p.setHealth(this.getHealth())); } + public void syncFood() { + this.getOnlinePlayers().stream().filter(p -> !p.isDead()).forEach(p -> p.setFoodLevel(this.getFood())); + } + public void addPlayerDeath(Player player) { UUID playerUUID = player.getUniqueId(); deathLeaderBoard.merge(playerUUID, 1, Integer::sum);