From 47053fde31008bcc8ff5bc071e50814888ecbe48 Mon Sep 17 00:00:00 2001 From: BONNe Date: Fri, 17 Jun 2022 14:40:10 +0300 Subject: [PATCH] Implement customizable Values GUI. (#262) This GUI shows value to all items in game. It also shows max limit of blocks, if it is set. Fixes of #192 --- pom.xml | 2 +- src/main/java/world/bentobox/level/Level.java | 1 + .../level/commands/IslandValueCommand.java | 136 ++- .../bentobox/level/panels/DetailsPanel.java | 105 +-- .../bentobox/level/panels/ValuePanel.java | 773 ++++++++++++++++++ .../level/util/ConversationUtils.java | 119 +++ .../java/world/bentobox/level/util/Utils.java | 91 +++ src/main/resources/locales/en-US.yml | 55 +- src/main/resources/panels/value_panel.yml | 109 +++ 9 files changed, 1263 insertions(+), 128 deletions(-) create mode 100644 src/main/java/world/bentobox/level/panels/ValuePanel.java create mode 100644 src/main/java/world/bentobox/level/util/ConversationUtils.java create mode 100644 src/main/resources/panels/value_panel.yml diff --git a/pom.xml b/pom.xml index fdc2584..b0c45ca 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ 1.16.5-R0.1-SNAPSHOT 1.20.0 - 1.0.0 + 1.1.0 ${build.version}-SNAPSHOT diff --git a/src/main/java/world/bentobox/level/Level.java b/src/main/java/world/bentobox/level/Level.java index 96e0714..33c76a0 100644 --- a/src/main/java/world/bentobox/level/Level.java +++ b/src/main/java/world/bentobox/level/Level.java @@ -78,6 +78,7 @@ public class Level extends Addon implements Listener { // Save existing panels. this.saveResource("panels/top_panel.yml", false); this.saveResource("panels/detail_panel.yml", false); + this.saveResource("panels/value_panel.yml", false); } private boolean loadSettings() { diff --git a/src/main/java/world/bentobox/level/commands/IslandValueCommand.java b/src/main/java/world/bentobox/level/commands/IslandValueCommand.java index ffdb0a7..7e04a53 100644 --- a/src/main/java/world/bentobox/level/commands/IslandValueCommand.java +++ b/src/main/java/world/bentobox/level/commands/IslandValueCommand.java @@ -1,6 +1,9 @@ package world.bentobox.level.commands; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Optional; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -8,43 +11,132 @@ import org.bukkit.inventory.PlayerInventory; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.util.Util; import world.bentobox.level.Level; +import world.bentobox.level.panels.ValuePanel; +import world.bentobox.level.util.Utils; -public class IslandValueCommand extends CompositeCommand { + +public class IslandValueCommand extends CompositeCommand +{ private final Level addon; - public IslandValueCommand(Level addon, CompositeCommand parent) { + + public IslandValueCommand(Level addon, CompositeCommand parent) + { super(parent, "value"); this.addon = addon; } + @Override - public void setup() { + public void setup() + { this.setPermission("island.value"); - this.setDescription("island.value.description"); + this.setParametersHelp("level.commands.value.parameters"); + this.setDescription("level.commands.value.description"); this.setOnlyPlayer(true); } + @Override - public boolean execute(User user, String label, List args) { - Player player = user.getPlayer(); - PlayerInventory inventory = player.getInventory(); - if (!inventory.getItemInMainHand().getType().equals(Material.AIR)) { - Material material = inventory.getItemInMainHand().getType(); - Integer value = addon.getBlockConfig().getValue(getWorld(), material); - if (value != null) { - user.sendMessage("island.value.success", "[value]", String.valueOf(value)); - double underWater = addon.getSettings().getUnderWaterMultiplier(); - if (underWater > 1.0) { - user.sendMessage("island.value.success-underwater", "[value]", (underWater * value) + ""); - } - } else { - user.sendMessage("island.value.no-value"); - } - } else { - user.sendMessage("island.value.empty-hand"); + public boolean execute(User user, String label, List args) + { + if (args.size() > 1) + { + this.showHelp(this, user); + return true; } + + if (args.isEmpty()) + { + ValuePanel.openPanel(this.addon, this.getWorld(), user); + } + else if (args.get(0).equalsIgnoreCase("HAND")) + { + Player player = user.getPlayer(); + PlayerInventory inventory = player.getInventory(); + + if (!inventory.getItemInMainHand().getType().equals(Material.AIR)) + { + this.printValue(user, inventory.getItemInMainHand().getType()); + } + else + { + Utils.sendMessage(user, user.getTranslation("level.conversations.empty-hand")); + } + } + else + { + Material material = Material.matchMaterial(args.get(0)); + + if (material == null) + { + Utils.sendMessage(user, + user.getTranslation(this.getWorld(), "level.conversations.unknown-item", + "[material]", args.get(0))); + } + else + { + this.printValue(user, material); + } + } + return true; } -} + + /** + * This method prints value of the given material in chat. + * @param user User who receives the message. + * @param material Material value. + */ + private void printValue(User user, Material material) + { + Integer value = this.addon.getBlockConfig().getValue(getWorld(), material); + + if (value != null) + { + Utils.sendMessage(user, + user.getTranslation(this.getWorld(), "level.conversations.value", + "[value]", String.valueOf(value), + "[material]", Utils.prettifyObject(material, user))); + + double underWater = this.addon.getSettings().getUnderWaterMultiplier(); + + if (underWater > 1.0) + { + Utils.sendMessage(user, + user.getTranslation(this.getWorld(),"level.conversations.success-underwater", + "[value]", (underWater * value) + ""), + "[material]", Utils.prettifyObject(material, user)); + } + } + else + { + Utils.sendMessage(user, + user.getTranslation(this.getWorld(),"level.conversations.no-value")); + } + } + + + @Override + public Optional> tabComplete(User user, String alias, List args) + { + String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; + + if (args.isEmpty()) + { + // Don't show every player on the server. Require at least the first letter + return Optional.empty(); + } + + List options = new ArrayList<>(Arrays.stream(Material.values()). + filter(Material::isBlock). + map(Material::name).toList()); + + options.add("HAND"); + + return Optional.of(Util.tabLimit(options, lastArg)); + } +} \ No newline at end of file diff --git a/src/main/java/world/bentobox/level/panels/DetailsPanel.java b/src/main/java/world/bentobox/level/panels/DetailsPanel.java index bbffd57..5a13682 100644 --- a/src/main/java/world/bentobox/level/panels/DetailsPanel.java +++ b/src/main/java/world/bentobox/level/panels/DetailsPanel.java @@ -165,8 +165,8 @@ public class DetailsPanel { if (o1.getValue().equals(o2.getValue())) { - String o1Name = DetailsPanel.prettifyObject(o1.getKey(), this.user); - String o2Name = DetailsPanel.prettifyObject(o2.getKey(), this.user); + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); } @@ -193,8 +193,8 @@ public class DetailsPanel if (o1Value == o2Value) { - String o1Name = DetailsPanel.prettifyObject(o1.getKey(), this.user); - String o2Name = DetailsPanel.prettifyObject(o2.getKey(), this.user); + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); } @@ -208,8 +208,8 @@ public class DetailsPanel { sorter = (o1, o2) -> { - String o1Name = DetailsPanel.prettifyObject(o1.getKey(), this.user); - String o2Name = DetailsPanel.prettifyObject(o2.getKey(), this.user); + String o1Name = Utils.prettifyObject(o1.getKey(), this.user); + String o2Name = Utils.prettifyObject(o2.getKey(), this.user); return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); }; @@ -647,10 +647,10 @@ public class DetailsPanel { builder.name(this.user.getTranslation(this.world, template.title(), "[number]", String.valueOf(materialCount.getValue()), - "[material]", DetailsPanel.prettifyObject(materialCount.getKey(), this.user))); + "[material]", Utils.prettifyObject(materialCount.getKey(), this.user))); } - String description = DetailsPanel.prettifyDescription(materialCount.getKey(), this.user); + String description = Utils.prettifyDescription(materialCount.getKey(), this.user); final String reference = "level.gui.buttons.material."; String blockId = this.user.getTranslationOrNothing(reference + "id", @@ -710,95 +710,6 @@ public class DetailsPanel } - /** - * Prettify Material object for user. - * @param object Object that must be pretty. - * @param user User who will see the object. - * @return Prettified string for Material. - */ - private static String prettifyObject(Material object, User user) - { - // Nothing to translate - if (object == null) - { - return ""; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: - // name: [name] - String translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase() + ".name"); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: [name] - - translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase()); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Find general structure with: - // materials: - // [material]: [name] - - translation = user.getTranslationOrNothing("materials." + object.name().toLowerCase()); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // Use Lang Utils Hook to translate material - return LangUtilsHook.getMaterialName(object, user); - } - - - /** - * Prettify Material object description for user. - * @param object Object that must be pretty. - * @param user User who will see the object. - * @return Prettified description string for Material. - */ - public static String prettifyDescription(Material object, User user) - { - // Nothing to translate - if (object == null) - { - return ""; - } - - // Find addon structure with: - // [addon]: - // materials: - // [material]: - // description: [text] - String translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase() + ".description"); - - if (!translation.isEmpty()) - { - // We found our translation. - return translation; - } - - // No text to return. - return ""; - } - - // --------------------------------------------------------------------- // Section: Enums // --------------------------------------------------------------------- diff --git a/src/main/java/world/bentobox/level/panels/ValuePanel.java b/src/main/java/world/bentobox/level/panels/ValuePanel.java new file mode 100644 index 0000000..fdc04ae --- /dev/null +++ b/src/main/java/world/bentobox/level/panels/ValuePanel.java @@ -0,0 +1,773 @@ +package world.bentobox.level.panels; + + +import com.google.common.base.Enums; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; +import java.io.File; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lv.id.bonne.panelutils.PanelUtils; +import world.bentobox.bentobox.api.panels.PanelItem; +import world.bentobox.bentobox.api.panels.TemplatedPanel; +import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder; +import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder; +import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord; +import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.hooks.LangUtilsHook; +import world.bentobox.bentobox.util.Pair; +import world.bentobox.level.Level; +import world.bentobox.level.util.ConversationUtils; +import world.bentobox.level.util.Utils; + + +/** + * This class opens GUI that shows generator view for user. + */ +public class ValuePanel +{ + // --------------------------------------------------------------------- + // Section: Internal Constructor + // --------------------------------------------------------------------- + + + /** + * This is internal constructor. It is used internally in current class to avoid creating objects everywhere. + * + * @param addon Level object + * @param world World where user is operating + * @param user User who opens panel + */ + private ValuePanel(Level addon, + World world, + User user) + { + this.addon = addon; + this.world = world; + this.user = user; + + this.activeFilter = Filter.NAME_ASC; + this.materialRecordList = Arrays.stream(Material.values()). + filter(Material::isBlock). + filter(m -> !m.name().startsWith("LEGACY_")). + map(material -> + { + Integer value = this.addon.getBlockConfig().getValue(this.world, material); + Integer limit = this.addon.getBlockConfig().getBlockLimits().get(material); + + return new MaterialRecord(material, + value != null ? value : 0, + limit != null ? limit : 0); + }). + collect(Collectors.toList()); + + this.elementList = new ArrayList<>(Material.values().length); + this.searchText = ""; + + this.updateFilters(); + } + + + /** + * This method builds this GUI. + */ + private void build() + { + // Start building panel. + TemplatedPanelBuilder panelBuilder = new TemplatedPanelBuilder(); + panelBuilder.user(this.user); + panelBuilder.world(this.user.getWorld()); + + panelBuilder.template("value_panel", new File(this.addon.getDataFolder(), "panels")); + + panelBuilder.registerTypeBuilder("NEXT", this::createNextButton); + panelBuilder.registerTypeBuilder("PREVIOUS", this::createPreviousButton); + panelBuilder.registerTypeBuilder("BLOCK", this::createMaterialButton); + + panelBuilder.registerTypeBuilder("FILTER", this::createFilterButton); + panelBuilder.registerTypeBuilder("SEARCH", this::createSearchButton); + + // Register unknown type builder. + panelBuilder.build(); + } + + + /** + * This method updates filter of elements based on tabs. + */ + private void updateFilters() + { + Comparator sorter; + + switch (this.activeFilter) + { + case VALUE_ASC -> + { + sorter = (o1, o2) -> + { + if (o1.value().equals(o2.value())) + { + String o1Name = Utils.prettifyObject(o1.material(), this.user); + String o2Name = Utils.prettifyObject(o2.material(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + } + else + { + return Integer.compare(o1.value(), o2.value()); + } + }; + } + case VALUE_DESC -> + { + sorter = (o1, o2) -> + { + if (o1.value().equals(o2.value())) + { + String o1Name = Utils.prettifyObject(o1.material(), this.user); + String o2Name = Utils.prettifyObject(o2.material(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + } + else + { + return Integer.compare(o2.value(), o1.value()); + } + }; + } + case NAME_DESC -> + { + sorter = (o1, o2) -> + { + String o1Name = Utils.prettifyObject(o1.material(), this.user); + String o2Name = Utils.prettifyObject(o2.material(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o2Name, o1Name); + }; + } + default -> + { + sorter = (o1, o2) -> + { + String o1Name = Utils.prettifyObject(o1.material(), this.user); + String o2Name = Utils.prettifyObject(o2.material(), this.user); + + return String.CASE_INSENSITIVE_ORDER.compare(o1Name, o2Name); + }; + } + } + + this.materialRecordList.sort(sorter); + + if (!this.searchText.isBlank()) + { + this.elementList = new ArrayList<>(this.materialRecordList.size()); + final String text = this.searchText.toLowerCase(); + + this.materialRecordList.forEach(record -> + { + if (record.material.name().toLowerCase().contains(text) || + Utils.prettifyObject(record.material(), this.user).toLowerCase().contains(text)) + { + this.elementList.add(record); + } + }); + } + else + { + this.elementList = this.materialRecordList; + } + + this.pageIndex = 0; + } + + +// --------------------------------------------------------------------- +// Section: Tab Button Type +// --------------------------------------------------------------------- + + + /** + * Create tab button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createSearchButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + // Set icon + builder.icon(template.icon().clone()); + } + + if (template.title() != null) + { + // Set title + builder.name(this.user.getTranslation(this.world, template.title(), "[text]", this.searchText)); + } + + if (template.description() != null) + { + // Set description + builder.description(this.user.getTranslation(this.world, template.description(), "[text]", this.searchText)); + } + + // Get only possible actions, by removing all inactive ones. + List activeActions = new ArrayList<>(template.actions()); + + activeActions.removeIf(action -> + "CLEAR".equalsIgnoreCase(action.actionType()) && this.searchText.isBlank()); + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> + { + for (ItemTemplateRecord.ActionRecords action : activeActions) + { + if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + { + if ("CLEAR".equalsIgnoreCase(action.actionType())) + { + this.searchText = ""; + + // Update filters. + this.updateFilters(); + this.build(); + } + else if ("INPUT".equalsIgnoreCase(action.actionType())) + { + // Create consumer that process description change + Consumer consumer = value -> + { + if (value != null) + { + this.searchText = value; + this.updateFilters(); + } + + this.build(); + }; + + // start conversation + ConversationUtils.createStringInput(consumer, + user, + user.getTranslation("level.conversations.write-search"), + user.getTranslation("level.conversations.search-updated")); + } + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = activeActions.stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + builder.glow(!this.searchText.isBlank()); + + return builder.build(); + } + + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createFilterButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + // Set icon + builder.icon(template.icon().clone()); + } + + String filterName = String.valueOf(template.dataMap().get("filter")); + + final String reference = "level.gui.buttons.filters."; + + if (template.title() != null) + { + // Set title + builder.name(this.user.getTranslation(this.world, template.title())); + } + else + { + builder.name(this.user.getTranslation(this.world, reference + filterName.toLowerCase() + ".name")); + } + + if (template.description() != null) + { + // Set description + builder.description(this.user.getTranslation(this.world, template.description())); + } + else + { + builder.name(this.user.getTranslation(this.world, reference + filterName.toLowerCase() + ".description")); + } + + // Get only possible actions, by removing all inactive ones. + List activeActions = new ArrayList<>(template.actions()); + + activeActions.removeIf(action -> { + if (this.activeFilter.name().startsWith(filterName)) + { + return this.activeFilter.name().endsWith("ASC") && "ASC".equalsIgnoreCase(action.actionType()) || + this.activeFilter.name().endsWith("DESC") && "DESC".equalsIgnoreCase(action.actionType()); + } + else + { + return "DESC".equalsIgnoreCase(action.actionType()); + } + }); + + // Add Click handler + builder.clickHandler((panel, user, clickType, i) -> + { + for (ItemTemplateRecord.ActionRecords action : activeActions) + { + if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + { + if ("ASC".equalsIgnoreCase(action.actionType())) + { + this.activeFilter = Enums.getIfPresent(Filter.class, filterName + "_ASC").or(Filter.NAME_ASC); + + // Update filters. + this.updateFilters(); + this.build(); + } + else if ("DESC".equalsIgnoreCase(action.actionType())) + { + this.activeFilter = Enums.getIfPresent(Filter.class, filterName + "_DESC").or(Filter.NAME_DESC); + + // Update filters. + this.updateFilters(); + this.build(); + } + } + } + + return true; + }); + + // Collect tooltips. + List tooltips = activeActions.stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + builder.glow(this.activeFilter.name().startsWith(filterName.toUpperCase())); + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Create common buttons +// --------------------------------------------------------------------- + + + /** + * Create next button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createNextButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + long size = this.elementList.size(); + + if (size <= slot.amountMap().getOrDefault("BLOCK", 1) || + 1.0 * size / slot.amountMap().getOrDefault("BLOCK", 1) <= this.pageIndex + 1) + { + // There are no next elements + return null; + } + + int nextPageIndex = this.pageIndex + 2; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(nextPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description(), + "[number]", String.valueOf(nextPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + for (ItemTemplateRecord.ActionRecords action : template.actions()) + { + if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + { + if ("NEXT".equalsIgnoreCase(action.actionType())) + { + this.pageIndex++; + this.build(); + } + } + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + + /** + * Create previous button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createPreviousButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.pageIndex == 0) + { + // There are no next elements + return null; + } + + int previousPageIndex = this.pageIndex; + + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + ItemStack clone = template.icon().clone(); + + if ((Boolean) template.dataMap().getOrDefault("indexing", false)) + { + clone.setAmount(previousPageIndex); + } + + builder.icon(clone); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title())); + } + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description(), + "[number]", String.valueOf(previousPageIndex))); + } + + // Add ClickHandler + builder.clickHandler((panel, user, clickType, i) -> + { + for (ItemTemplateRecord.ActionRecords action : template.actions()) + { + if (clickType == action.clickType() || ClickType.UNKNOWN.equals(action.clickType())) + { + if ("PREVIOUS".equalsIgnoreCase(action.actionType())) + { + this.pageIndex--; + this.build(); + } + } + } + + // Always return true. + return true; + }); + + // Collect tooltips. + List tooltips = template.actions().stream(). + filter(action -> action.tooltip() != null). + map(action -> this.user.getTranslation(this.world, action.tooltip())). + filter(text -> !text.isBlank()). + collect(Collectors.toCollection(() -> new ArrayList<>(template.actions().size()))); + + // Add tooltips. + if (!tooltips.isEmpty()) + { + // Empty line and tooltips. + builder.description(""); + builder.description(tooltips); + } + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Create Material Button +// --------------------------------------------------------------------- + + + /** + * Create material button panel item. + * + * @param template the template + * @param slot the slot + * @return the panel item + */ + private PanelItem createMaterialButton(ItemTemplateRecord template, TemplatedPanel.ItemSlot slot) + { + if (this.elementList.isEmpty()) + { + // Does not contain any generators. + return null; + } + + int index = this.pageIndex * slot.amountMap().getOrDefault("BLOCK", 1) + slot.slot(); + + if (index >= this.elementList.size()) + { + // Out of index. + return null; + } + + return this.createMaterialButton(template, this.elementList.get(index)); + } + + + /** + * This method creates button for material. + * + * @param template the template of the button + * @param materialRecord materialRecord which button must be created. + * @return PanelItem for generator tier. + */ + private PanelItem createMaterialButton(ItemTemplateRecord template, + MaterialRecord materialRecord) + { + PanelItemBuilder builder = new PanelItemBuilder(); + + if (template.icon() != null) + { + builder.icon(template.icon().clone()); + } + else + { + builder.icon(PanelUtils.getMaterialItem(materialRecord.material())); + } + + if (materialRecord.value() <= 64 && materialRecord.value() > 0) + { + builder.amount(materialRecord.value()); + } + + if (template.title() != null) + { + builder.name(this.user.getTranslation(this.world, template.title(), + "[material]", Utils.prettifyObject(materialRecord.material(), this.user))); + } + + String description = Utils.prettifyDescription(materialRecord.material(), this.user); + + final String reference = "level.gui.buttons.material."; + String blockId = this.user.getTranslationOrNothing(reference + "id", + "[id]", materialRecord.material().name()); + + String value = this.user.getTranslationOrNothing(reference + "value", + "[number]", String.valueOf(materialRecord.value())); + + String underWater; + + if (this.addon.getSettings().getUnderWaterMultiplier() > 1.0) + { + underWater = this.user.getTranslationOrNothing(reference + "underwater", + "[number]", String.valueOf(materialRecord.value() * this.addon.getSettings().getUnderWaterMultiplier())); + } + else + { + underWater = ""; + } + + String limit = materialRecord.limit() > 0 ? this.user.getTranslationOrNothing(reference + "limit", + "[number]", String.valueOf(materialRecord.limit())) : ""; + + if (template.description() != null) + { + builder.description(this.user.getTranslation(this.world, template.description(), + "[description]", description, + "[id]", blockId, + "[value]", value, + "[underwater]", underWater, + "[limit]", limit). + replaceAll("(?m)^[ \\t]*\\r?\\n", ""). + replaceAll("(? { + System.out.println("Material: " + materialRecord.material()); + return true; + }); + + return builder.build(); + } + + +// --------------------------------------------------------------------- +// Section: Other Methods +// --------------------------------------------------------------------- + + + /** + * This method is used to open UserPanel outside this class. It will be much easier to open panel with single method + * call then initializing new object. + * + * @param addon Level object + * @param world World where user is operating + * @param user User who opens panel + */ + public static void openPanel(Level addon, + World world, + User user) + { + new ValuePanel(addon, world, user).build(); + } + + +// --------------------------------------------------------------------- +// Section: Enums +// --------------------------------------------------------------------- + + + /** + * Sorting order of blocks. + */ + private enum Filter + { + /** + * By name asc + */ + NAME_ASC, + /** + * By name desc + */ + NAME_DESC, + /** + * By value asc + */ + VALUE_ASC, + /** + * By value desc + */ + VALUE_DESC, + } + + + private record MaterialRecord(Material material, Integer value, Integer limit) + { + } + + +// --------------------------------------------------------------------- +// Section: Variables +// --------------------------------------------------------------------- + + /** + * This variable allows to access addon object. + */ + private final Level addon; + + /** + * This variable holds user who opens panel. Without it panel cannot be opened. + */ + private final User user; + + /** + * This variable holds a world to which gui referee. + */ + private final World world; + + /** + * This variable stores the list of elements to display. + */ + private final List materialRecordList; + + /** + * This variable stores the list of elements to display. + */ + private List elementList; + + /** + * This variable holds current pageIndex for multi-page generator choosing. + */ + private int pageIndex; + + /** + * This variable stores which tab currently is active. + */ + private String searchText; + + /** + * This variable stores active filter for items. + */ + private Filter activeFilter; +} diff --git a/src/main/java/world/bentobox/level/util/ConversationUtils.java b/src/main/java/world/bentobox/level/util/ConversationUtils.java new file mode 100644 index 0000000..ad9dbcc --- /dev/null +++ b/src/main/java/world/bentobox/level/util/ConversationUtils.java @@ -0,0 +1,119 @@ +// +// Created by BONNe +// Copyright - 2021 +// + + +package world.bentobox.level.util; + + +import org.bukkit.conversations.*; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import java.util.function.Consumer; + +import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.user.User; + + +public class ConversationUtils +{ +// --------------------------------------------------------------------- +// Section: Conversation API implementation +// --------------------------------------------------------------------- + + + /** + * This method will close opened gui and writes question in chat. After players answers on question in chat, message + * will trigger consumer and gui will reopen. + * + * @param consumer Consumer that accepts player output text. + * @param question Message that will be displayed in chat when player triggers conversion. + * @param user User who is targeted with current confirmation. + */ + public static void createStringInput(Consumer consumer, + User user, + @NonNull String question, + @Nullable String successMessage) + { + // Text input message. + StringPrompt stringPrompt = new StringPrompt() + { + @Override + public @NonNull String getPromptText(@NonNull ConversationContext context) + { + user.closeInventory(); + return question; + } + + + @Override + public @NonNull Prompt acceptInput(@NonNull ConversationContext context, @Nullable String input) + { + consumer.accept(input); + return ConversationUtils.endMessagePrompt(successMessage); + } + }; + + new ConversationFactory(BentoBox.getInstance()). + withPrefix(context -> user.getTranslation("level.conversations.prefix")). + withFirstPrompt(stringPrompt). + // On cancel conversation will be closed. + withLocalEcho(false). + withTimeout(90). + withEscapeSequence(user.getTranslation("level.conversations.cancel-string")). + // Use null value in consumer to detect if user has abandoned conversation. + addConversationAbandonedListener(ConversationUtils.getAbandonListener(consumer, user)). + buildConversation(user.getPlayer()). + begin(); + } + + + /** + * This is just a simple end message prompt that displays requested message. + * + * @param message Message that will be displayed. + * @return MessagePrompt that displays given message and exists from conversation. + */ + private static MessagePrompt endMessagePrompt(@Nullable String message) + { + return new MessagePrompt() + { + @Override + public @NonNull String getPromptText(@NonNull ConversationContext context) + { + return message == null ? "" : message; + } + + + @Override + protected @Nullable Prompt getNextPrompt(@NonNull ConversationContext context) + { + return Prompt.END_OF_CONVERSATION; + } + }; + } + + + /** + * This method creates and returns abandon listener for every conversation. + * + * @param consumer Consumer which must return null value. + * @param user User who was using conversation. + * @return ConversationAbandonedListener instance. + */ + private static ConversationAbandonedListener getAbandonListener(Consumer consumer, User user) + { + return abandonedEvent -> + { + if (!abandonedEvent.gracefulExit()) + { + consumer.accept(null); + // send cancell message + abandonedEvent.getContext().getForWhom().sendRawMessage( + user.getTranslation("level.conversations.prefix") + + user.getTranslation("level.conversations.cancelled")); + } + }; + } +} diff --git a/src/main/java/world/bentobox/level/util/Utils.java b/src/main/java/world/bentobox/level/util/Utils.java index 45bc002..6d177a1 100644 --- a/src/main/java/world/bentobox/level/util/Utils.java +++ b/src/main/java/world/bentobox/level/util/Utils.java @@ -7,11 +7,13 @@ package world.bentobox.level.util; +import org.bukkit.Material; import org.bukkit.permissions.PermissionAttachmentInfo; import java.util.List; import java.util.stream.Collectors; import world.bentobox.bentobox.api.user.User; +import world.bentobox.bentobox.hooks.LangUtilsHook; public class Utils @@ -131,4 +133,93 @@ public class Utils return currentValue; } + + + /** + * Prettify Material object for user. + * @param object Object that must be pretty. + * @param user User who will see the object. + * @return Prettified string for Material. + */ + public static String prettifyObject(Material object, User user) + { + // Nothing to translate + if (object == null) + { + return ""; + } + + // Find addon structure with: + // [addon]: + // materials: + // [material]: + // name: [name] + String translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase() + ".name"); + + if (!translation.isEmpty()) + { + // We found our translation. + return translation; + } + + // Find addon structure with: + // [addon]: + // materials: + // [material]: [name] + + translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase()); + + if (!translation.isEmpty()) + { + // We found our translation. + return translation; + } + + // Find general structure with: + // materials: + // [material]: [name] + + translation = user.getTranslationOrNothing("materials." + object.name().toLowerCase()); + + if (!translation.isEmpty()) + { + // We found our translation. + return translation; + } + + // Use Lang Utils Hook to translate material + return LangUtilsHook.getMaterialName(object, user); + } + + + /** + * Prettify Material object description for user. + * @param object Object that must be pretty. + * @param user User who will see the object. + * @return Prettified description string for Material. + */ + public static String prettifyDescription(Material object, User user) + { + // Nothing to translate + if (object == null) + { + return ""; + } + + // Find addon structure with: + // [addon]: + // materials: + // [material]: + // description: [text] + String translation = user.getTranslationOrNothing("level.materials." + object.name().toLowerCase() + ".description"); + + if (!translation.isEmpty()) + { + // We found our translation. + return translation; + } + + // No text to return. + return ""; + } } diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index dd2e73b..af9d584 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -55,18 +55,16 @@ island: syntax: "[name] x [number]" hint: "&c Run level to see the block report" - value: - description: "shows the value of any block" - success: "&7 The value of this block is: &e[value]" - success-underwater: "&7 The value of this block below sea-level: &e[value]" - empty-hand: "&c There are no blocks in your hand" - no-value: "&c That item has no value." - level: + commands: + value: + parameters: "[hand|]" + description: "shows the value of blocks. Add 'hand' at the end to display value for item in hand." gui: titles: top: "&0&l Top Islands" detail-panel: "&0&l [name]'s island" + value-panel: "&0&l Block Values" buttons: island: empty: '&f&l [name]. place' @@ -138,6 +136,18 @@ level: name: "&f&l Sort by Count" description: |- &7 Sort all blocks by their amount. + value: + name: "&f&l [material]" + description: |- + [description] + [value] + [underwater] + [limit] + [id] + id: "&7 Block id: &e [id]" + value: "&7 Block value: &e [number]" + underwater: "&7 Bellow sea level: &e [number]" + limit: "&7 Block limit: &e [number]" # Button that is used in multi-page GUIs which allows to return to previous page. previous: name: "&f&l Previous Page" @@ -148,6 +158,12 @@ level: name: "&f&l Next Page" description: |- &7 Switch to [number] page + search: + name: "&f&l Search" + description: |- + &7 Search for a specific + &7 value. + search: "&b Value: [value]" tips: click-to-view: "&e Click &7 to view." click-to-previous: "&e Click &7 to view previous page." @@ -155,7 +171,30 @@ level: click-to-select: "&e Click &7 to select." left-click-to-cycle-up: "&e Left Click &7 to cycle up." right-click-to-cycle-down: "&e Right Click &7 to cycle down." + left-click-to-change: "&e Left Click &7 to edit." + right-click-to-clear: "&e Right Click &7 to clear." + click-to-asc: "&e Click &7 to sort in increasing order." + click-to-desc: "&e Click &7 to sort in decreasing order." conversations: # Prefix for messages that are send from server. prefix: "&l&6 [BentoBox]: &r" - no-data: "&c Run level to see the block report." \ No newline at end of file + no-data: "&c Run level to see the block report." + # String that allows to cancel conversation. (can be only one) + cancel-string: "cancel" + # List of strings that allows to exit conversation. (separated with ,) + exit-string: "cancel, exit, quit" + # Message that asks for search value input. + write-search: "&e Please enter a search value. (Write 'cancel' to exit)" + # Message that appears after updating search value. + search-updated: "&a Search value updated." + # Message that is sent to user when conversation is cancelled. + cancelled: "&c Conversation cancelled!" + # Message that is sent to user when given material does not have any value. + no-value: "&c That item has no value." + # Message that is sent to user when requested material does not exist. + unknown-item: "&c The '[material]' does not exist in game." + # Messages that is sent to user when requesting value for a specific material. + value: "&7 The value of '[material]' is: &e[value]" + value-underwater: "&7 The value of '[material]' below sea-level: &e[value]" + # Message that is sent to user when he does not hold any items in hand. + empty-hand: "&c There are no blocks in your hand" diff --git a/src/main/resources/panels/value_panel.yml b/src/main/resources/panels/value_panel.yml new file mode 100644 index 0000000..c173313 --- /dev/null +++ b/src/main/resources/panels/value_panel.yml @@ -0,0 +1,109 @@ +value_panel: + title: level.gui.titles.value-panel + type: INVENTORY + background: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + border: + icon: BLACK_STAINED_GLASS_PANE + title: "&b&r" # Empty text + force-shown: [] + content: + 1: + 4: + icon: PAPER + title: level.gui.buttons.filters.name.name + description: level.gui.buttons.filters.name.description + data: + type: FILTER + # the value of filter button. Suggestion is to leave fist value to name if you use single button. + filter: NAME + actions: + asc: + click-type: unknown + tooltip: level.gui.tips.click-to-asc + desc: + click-type: unknown + tooltip: level.gui.tips.click-to-desc + 5: + # You can create multiple buttons. By default it is one. + icon: MAP + title: level.gui.buttons.search.name + description: level.gui.buttons.search.description + data: + type: SEARCH + actions: + input: + click-type: left + tooltip: level.gui.tips.left-click-to-change + clear: + click-type: right + tooltip: level.gui.tips.right-click-to-clear + 6: + icon: DIAMOND + title: level.gui.buttons.filters.value.name + description: level.gui.buttons.filters.value.description + data: + type: FILTER + # the value of filter button. Suggestion is to leave fist value to name if you use single button. + filter: VALUE + actions: + asc: + click-type: unknown + tooltip: level.gui.tips.click-to-asc + desc: + click-type: unknown + tooltip: level.gui.tips.click-to-desc + 2: + 2: material_button + 3: material_button + 4: material_button + 5: material_button + 6: material_button + 7: material_button + 8: material_button + 3: + 1: + icon: TIPPED_ARROW:INSTANT_HEAL::::1 + title: level.gui.buttons.previous.name + description: level.gui.buttons.previous.description + data: + type: PREVIOUS + indexing: true + actions: + previous: + click-type: unknown + tooltip: level.gui.tips.click-to-previous + 2: material_button + 3: material_button + 4: material_button + 5: material_button + 6: material_button + 7: material_button + 8: material_button + 9: + icon: TIPPED_ARROW:JUMP::::1 + title: level.gui.buttons.next.name + description: level.gui.buttons.next.description + data: + type: NEXT + indexing: true + actions: + next: + click-type: unknown + tooltip: level.gui.tips.click-to-next + 4: + 2: material_button + 3: material_button + 4: material_button + 5: material_button + 6: material_button + 7: material_button + 8: material_button + reusable: + material_button: + #icon: STONE + title: level.gui.buttons.value.name + description: level.gui.buttons.value.description + data: + type: BLOCK \ No newline at end of file