new progress GUI API
This commit is contained in:
1 parent
e606c9d638
commit
cc77691f2e
8 files changed
+625
-583
No files matched your search
@@ -73,34 +73,48 @@ Notes:
|
||||
* A `GUIButton` method should return `true` when the click was handled.
|
||||
* Non-button items can call `item.playErrorSound(false)` to make clicks silent.
|
||||
|
||||
## Advancement API
|
||||
## Progress menu API
|
||||
|
||||
`AdvancementMenu` displays an ordered list of read-only advancement items in a paginated inventory. Items are placed
|
||||
from the top-left slot downward through each column, reserving the bottom row for previous, close, and next controls.
|
||||
`ProgressMenu` displays a layered, read-only progress inventory. The first layer lists sections. Clicking a section
|
||||
opens a second layer containing one material-backed item per goal. Hovering a goal shows its completion state and,
|
||||
for counter goals, its current and target values. Both layers paginate automatically.
|
||||
|
||||
Java:
|
||||
|
||||
```java
|
||||
Map<String, List<String>> values = new LinkedHashMap<>();
|
||||
values.put("&cHome Two", List.of("&7Unlock the second home.", "&eRequired: &fWin 1 duel"));
|
||||
values.put("&cHome Three", List.of("&7Unlock the third home.", "&eRequired: &fBuild a full beacon"));
|
||||
ProgressMenu.builder(plugin, player, "&aHome Quests")
|
||||
.section("Home 2 Quest", ProgressMenu.State.IN_PROGRESS)
|
||||
.progress(Material.DIAMOND, "Total Shards", 450, 900)
|
||||
.progress(Material.CLOCK, "Hours Played", 12, 24)
|
||||
.description("&7Use &e/claimquest &7when ready.")
|
||||
.section("Home 6 Quest", ProgressMenu.State.NOT_STARTED)
|
||||
.goal(Material.RED_CONCRETE, "How Did We Get Here?", false)
|
||||
.open();
|
||||
```
|
||||
|
||||
AdvancementMenu.open(plugin, player, "&6Advancements", values);
|
||||
Required imports:
|
||||
|
||||
```java
|
||||
import net.trueog.gxui.progress.ProgressMenu;
|
||||
import org.bukkit.Material;
|
||||
```
|
||||
|
||||
Kotlin:
|
||||
|
||||
```kotlin
|
||||
val values = linkedMapOf(
|
||||
"&cHome Two" to listOf("&7Unlock the second home.", "&eRequired: &fWin 1 duel"),
|
||||
"&cHome Three" to listOf("&7Unlock the third home.", "&eRequired: &fBuild a full beacon"),
|
||||
)
|
||||
|
||||
AdvancementMenu.open(plugin, player, "&6Advancements", values)
|
||||
ProgressMenu.builder(plugin, player, "&aHome Quests")
|
||||
.section("Home 2 Quest", ProgressMenu.State.IN_PROGRESS)
|
||||
.progress(Material.DIAMOND, "Total Shards", 450, 900)
|
||||
.progress(Material.CLOCK, "Hours Played", 12, 24)
|
||||
.description("&7Use &e/claimquest &7when ready.")
|
||||
.section("Home 6 Quest", ProgressMenu.State.NOT_STARTED)
|
||||
.goal(Material.RED_CONCRETE, "How Did We Get Here?", false)
|
||||
.open()
|
||||
```
|
||||
|
||||
The simple map API displays every entry as `RED_WOOL`. Use an ordered map, such as `LinkedHashMap`, when item order
|
||||
matters. Callers that need custom state or icons can still pass `List<AdvancementItem>` directly.
|
||||
Section icons are always state wool: red for `NOT_STARTED`, yellow for `IN_PROGRESS`, and lime for `COMPLETE`. Goal
|
||||
icons never change with state; completion is communicated through the display name and hover lore. This keeps each
|
||||
block related to the goal it represents while making the first layer immediately scannable.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ kotlin { jvmToolchain(17) }
|
||||
/* ----------------------------- Metadata ------------------------------ */
|
||||
group = "net.trueog.gxui-og" // Declare bundle identifier.
|
||||
|
||||
version = "1.3.3" // Declare plugin version (will be in .jar).
|
||||
version = "2.0.0" // Declare plugin version (will be in .jar).
|
||||
|
||||
val apiVersion = "1.19" // Declare minecraft server target version.
|
||||
|
||||
|
||||
@@ -422,4 +422,4 @@ public abstract class GUIBase implements Listener {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package net.trueog.gxui.advancements;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
// One requirement entry beneath an advancement. Built by AdvancementMenu.Builder.
|
||||
final class AdvancementAttribute {
|
||||
|
||||
private final String name;
|
||||
private final boolean done;
|
||||
private final long current;
|
||||
private final long target;
|
||||
private final boolean hasProgress;
|
||||
|
||||
AdvancementAttribute(String name, boolean done) {
|
||||
|
||||
this.name = Objects.requireNonNull(name, "name");
|
||||
this.done = done;
|
||||
this.current = 0L;
|
||||
this.target = 0L;
|
||||
this.hasProgress = false;
|
||||
|
||||
}
|
||||
|
||||
AdvancementAttribute(String name, long current, long target) {
|
||||
|
||||
this.name = Objects.requireNonNull(name, "name");
|
||||
this.current = current;
|
||||
this.target = target;
|
||||
this.done = current >= target;
|
||||
this.hasProgress = true;
|
||||
|
||||
}
|
||||
|
||||
String getName() {
|
||||
|
||||
return name;
|
||||
|
||||
}
|
||||
|
||||
boolean isDone() {
|
||||
|
||||
return done;
|
||||
|
||||
}
|
||||
|
||||
boolean hasProgress() {
|
||||
|
||||
return hasProgress;
|
||||
|
||||
}
|
||||
|
||||
long getCurrent() {
|
||||
|
||||
return current;
|
||||
|
||||
}
|
||||
|
||||
long getTarget() {
|
||||
|
||||
return target;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.trueog.gxui.advancements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
record AdvancementItem(String name, AdvancementState state, List<AdvancementAttribute> attributes,
|
||||
List<String> footer)
|
||||
{
|
||||
|
||||
AdvancementItem(String name, AdvancementState state, List<AdvancementAttribute> attributes, List<String> footer) {
|
||||
|
||||
this.name = Objects.requireNonNull(name, "name");
|
||||
this.state = Objects.requireNonNull(state, "state");
|
||||
this.attributes = List.copyOf(Objects.requireNonNull(attributes, "attributes"));
|
||||
this.footer = List.copyOf(Objects.requireNonNull(footer, "footer"));
|
||||
|
||||
}
|
||||
|
||||
String getName() {
|
||||
|
||||
return name;
|
||||
|
||||
}
|
||||
|
||||
AdvancementState getState() {
|
||||
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
List<AdvancementAttribute> getAttributes() {
|
||||
|
||||
return attributes;
|
||||
|
||||
}
|
||||
|
||||
List<String> getFooter() {
|
||||
|
||||
return footer;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,418 +0,0 @@
|
||||
package net.trueog.gxui.advancements;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.trueog.gxui.GUIBase;
|
||||
import net.trueog.gxui.GUIButton;
|
||||
import net.trueog.gxui.GUIItem;
|
||||
|
||||
// Read-only paginated inventory for advancement-style progress displays.
|
||||
// Construct with AdvancementMenu.builder(plugin, player, title). Run open() to display.
|
||||
public class AdvancementMenu extends GUIBase {
|
||||
|
||||
public static final int INVENTORY_SIZE = 54;
|
||||
public static final int ADVANCEMENTS_PER_PAGE = 45;
|
||||
public static final int PREVIOUS_PAGE_SLOT = 45;
|
||||
public static final int CLOSE_SLOT = 49;
|
||||
public static final int NEXT_PAGE_SLOT = 53;
|
||||
|
||||
private static final List<Integer> ADVANCEMENT_SLOTS = createAdvancementSlots();
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final String baseTitle;
|
||||
private final List<AdvancementItem> advancementItems;
|
||||
private final int page;
|
||||
|
||||
private AdvancementMenu(JavaPlugin plugin, Player player, String inventoryName,
|
||||
List<AdvancementItem> advancementItems, int page)
|
||||
{
|
||||
|
||||
super(plugin, player, buildInventoryName(inventoryName, advancementItems, page), INVENTORY_SIZE, true);
|
||||
this.plugin = plugin;
|
||||
this.baseTitle = inventoryName;
|
||||
this.advancementItems = advancementItems;
|
||||
this.page = normalizePage(page, getTotalPages(advancementItems.size()));
|
||||
|
||||
}
|
||||
|
||||
public static Builder builder(JavaPlugin plugin, Player player, String inventoryName) {
|
||||
|
||||
return new Builder(plugin, player, inventoryName);
|
||||
|
||||
}
|
||||
|
||||
public int getPage() {
|
||||
|
||||
return page;
|
||||
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
|
||||
return getTotalPages(advancementItems.size());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupItems() {
|
||||
|
||||
final int firstAdvancement = page * ADVANCEMENTS_PER_PAGE;
|
||||
final int lastAdvancement = Math.min(firstAdvancement + ADVANCEMENTS_PER_PAGE, advancementItems.size());
|
||||
for (int advancementIndex = firstAdvancement; advancementIndex < lastAdvancement; advancementIndex++) {
|
||||
|
||||
final int pageIndex = advancementIndex - firstAdvancement;
|
||||
addItem(ADVANCEMENT_SLOTS.get(pageIndex), buildAdvancementItem(advancementItems.get(advancementIndex)));
|
||||
|
||||
}
|
||||
|
||||
if (page > 0) {
|
||||
|
||||
addItem(PREVIOUS_PAGE_SLOT, buildPageButton(Material.ARROW, "&ePrevious Page", page - 1));
|
||||
|
||||
}
|
||||
|
||||
addItem(CLOSE_SLOT, buildCloseButton());
|
||||
|
||||
if (page + 1 < getTotalPages()) {
|
||||
|
||||
addItem(NEXT_PAGE_SLOT, buildPageButton(Material.ARROW, "&eNext Page", page + 1));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildAdvancementItem(AdvancementItem advancementItem) {
|
||||
|
||||
final AdvancementState state = advancementItem.getState();
|
||||
final String displayName = state.getColor() + advancementItem.getName();
|
||||
final GUIItem guiItem = new GUIItem(state.getDefaultMaterial(), 1, displayName);
|
||||
|
||||
guiItem.lore(toLoreComponents(buildLore(advancementItem)));
|
||||
guiItem.playErrorSound(false);
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private static List<String> buildLore(AdvancementItem advancementItem) {
|
||||
|
||||
final List<String> lore = new ArrayList<>();
|
||||
lore.add(advancementItem.getState().getStatusLine());
|
||||
|
||||
final List<AdvancementAttribute> attributes = advancementItem.getAttributes();
|
||||
if (!attributes.isEmpty()) {
|
||||
|
||||
lore.add("");
|
||||
lore.add("&7Requirements:");
|
||||
attributes.forEach(attribute -> lore.add(formatAttribute(attribute)));
|
||||
|
||||
}
|
||||
|
||||
final List<String> footer = advancementItem.getFooter();
|
||||
if (!footer.isEmpty()) {
|
||||
|
||||
lore.add("");
|
||||
lore.addAll(footer);
|
||||
|
||||
}
|
||||
|
||||
return lore;
|
||||
|
||||
}
|
||||
|
||||
private static String formatAttribute(AdvancementAttribute attribute) {
|
||||
|
||||
final String color = attribute.isDone() ? "&a" : "&c";
|
||||
if (attribute.hasProgress()) {
|
||||
|
||||
return color + attribute.getName() + ": &f" + attribute.getCurrent() + "&7/&f" + attribute.getTarget();
|
||||
|
||||
}
|
||||
|
||||
final String status = attribute.isDone() ? "Done" : "Not done";
|
||||
return color + attribute.getName() + ": &f" + status;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildCloseButton() {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(Material.BARRIER, 1, "&cClose");
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> getPlayer().closeInventory()));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildPageButton(Material material, String displayName, int targetPage) {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(material, 1, displayName);
|
||||
guiItem.lore(toLoreComponents(List.of("&7Page " + (targetPage + 1) + " of " + getTotalPages())));
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> openPage(targetPage)));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private void openPage(int targetPage) {
|
||||
|
||||
new AdvancementMenu(plugin, getPlayer(), baseTitle, advancementItems, targetPage).open(false);
|
||||
|
||||
}
|
||||
|
||||
private GUIButton createButton(Runnable clickHandler) {
|
||||
|
||||
return new GUIButton() {
|
||||
|
||||
@Override
|
||||
public boolean leftClick() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean leftClickShift() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rightClick() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rightClickShift() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
private boolean handleClick() {
|
||||
|
||||
clickHandler.run();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacyAmpersand();
|
||||
|
||||
private List<TextComponent> toLoreComponents(List<String> lore) {
|
||||
|
||||
return lore.stream().map(AdvancementMenu::toTextComponent).toList();
|
||||
|
||||
}
|
||||
|
||||
private static TextComponent toTextComponent(String line) {
|
||||
|
||||
final Component component = LEGACY.deserialize(line);
|
||||
// LegacyComponentSerializer always produces a TextComponent root.
|
||||
return (TextComponent) component;
|
||||
|
||||
}
|
||||
|
||||
private static String buildInventoryName(String inventoryName, List<AdvancementItem> advancementItems, int page) {
|
||||
|
||||
Objects.requireNonNull(inventoryName, "inventoryName");
|
||||
final int totalPages = getTotalPages(Objects.requireNonNull(advancementItems, "advancementItems").size());
|
||||
if (totalPages == 1) {
|
||||
|
||||
return inventoryName;
|
||||
|
||||
}
|
||||
|
||||
final int normalizedPage = normalizePage(page, totalPages);
|
||||
return inventoryName + " &7(" + (normalizedPage + 1) + "/" + totalPages + ")";
|
||||
|
||||
}
|
||||
|
||||
private static int getTotalPages(int itemCount) {
|
||||
|
||||
return Math.max(1, (int) Math.ceil((double) itemCount / ADVANCEMENTS_PER_PAGE));
|
||||
|
||||
}
|
||||
|
||||
private static int normalizePage(int page, int totalPages) {
|
||||
|
||||
if (page < 0) {
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
return page >= totalPages ? totalPages - 1 : page;
|
||||
|
||||
}
|
||||
|
||||
private static List<Integer> createAdvancementSlots() {
|
||||
|
||||
final List<Integer> slots = new ArrayList<>(ADVANCEMENTS_PER_PAGE);
|
||||
// Row-first ordering: fill left-to-right across the top row, then wrap down.
|
||||
for (int row = 0; row < 5; row++) {
|
||||
|
||||
for (int column = 0; column < 9; column++) {
|
||||
|
||||
slots.add(row * 9 + column);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(slots);
|
||||
|
||||
}
|
||||
|
||||
private static int parsePage(String[] args) {
|
||||
|
||||
if (args == null || args.length == 0) {
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
return Math.max(0, Integer.parseInt(args[0]) - 1);
|
||||
|
||||
} catch (NumberFormatException ignored) {
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fluent builder for an AdvancementMenu. Plugins only depend on this class,
|
||||
// AdvancementState, and Java primitives.
|
||||
public static final class Builder {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final Player player;
|
||||
private final String title;
|
||||
private final List<AdvancementItem> items = new ArrayList<>();
|
||||
|
||||
private String pendingName;
|
||||
private AdvancementState pendingState;
|
||||
private List<AdvancementAttribute> pendingAttributes;
|
||||
private List<String> pendingFooter;
|
||||
|
||||
private Builder(JavaPlugin plugin, Player player, String title) {
|
||||
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
this.player = Objects.requireNonNull(player, "player");
|
||||
this.title = Objects.requireNonNull(title, "title");
|
||||
|
||||
}
|
||||
|
||||
// Start a new advancement slot. Following attribute/footer calls attach to this
|
||||
// one.
|
||||
public Builder add(String name, AdvancementState state) {
|
||||
|
||||
flushPending();
|
||||
this.pendingName = Objects.requireNonNull(name, "name");
|
||||
this.pendingState = Objects.requireNonNull(state, "state");
|
||||
this.pendingAttributes = new ArrayList<>();
|
||||
this.pendingFooter = new ArrayList<>();
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// Add a progress-counter attribute (e.g. 250/1000). Coloured by current >=
|
||||
// target.
|
||||
public Builder progress(String name, long current, long target) {
|
||||
|
||||
requirePending();
|
||||
pendingAttributes.add(new AdvancementAttribute(name, current, target));
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// Add a boolean attribute.
|
||||
public Builder bool(String name, boolean done) {
|
||||
|
||||
requirePending();
|
||||
pendingAttributes.add(new AdvancementAttribute(name, done));
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// Append free-form footer lines (formatting allowed).
|
||||
public Builder footer(String... lines) {
|
||||
|
||||
requirePending();
|
||||
Collections.addAll(pendingFooter, lines);
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// Open at the page parsed from command args (e.g. "/cmd 2").
|
||||
public AdvancementMenu open(String[] args) {
|
||||
|
||||
return openAt(parsePage(args));
|
||||
|
||||
}
|
||||
|
||||
// Open at page 1.
|
||||
public AdvancementMenu open() {
|
||||
|
||||
return openAt(0);
|
||||
|
||||
}
|
||||
|
||||
private AdvancementMenu openAt(int page) {
|
||||
|
||||
flushPending();
|
||||
final AdvancementMenu menu = new AdvancementMenu(plugin, player, title, List.copyOf(items), page);
|
||||
menu.open(false);
|
||||
return menu;
|
||||
|
||||
}
|
||||
|
||||
private void requirePending() {
|
||||
|
||||
if (pendingName == null) {
|
||||
|
||||
throw new IllegalStateException("Call add(name, state) before adding attributes or footer lines");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void flushPending() {
|
||||
|
||||
if (pendingName == null) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
items.add(new AdvancementItem(pendingName, pendingState, pendingAttributes, pendingFooter));
|
||||
pendingName = null;
|
||||
pendingState = null;
|
||||
pendingAttributes = null;
|
||||
pendingFooter = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package net.trueog.gxui.advancements;
|
||||
|
||||
import org.bukkit.Material;
|
||||
|
||||
// Visual state of an advancement slot. Drives material colour and lore status line.
|
||||
public enum AdvancementState {
|
||||
|
||||
COMPLETE(Material.LIME_WOOL, "&a", "&aCompleted"), IN_PROGRESS(Material.YELLOW_WOOL, "&e", "&eIn Progress"),
|
||||
LOCKED(Material.RED_WOOL, "&c", "&cLocked");
|
||||
|
||||
private final Material defaultMaterial;
|
||||
private final String color;
|
||||
private final String statusLine;
|
||||
|
||||
AdvancementState(Material defaultMaterial, String color, String statusLine) {
|
||||
|
||||
this.defaultMaterial = defaultMaterial;
|
||||
this.color = color;
|
||||
this.statusLine = statusLine;
|
||||
|
||||
}
|
||||
|
||||
public Material getDefaultMaterial() {
|
||||
|
||||
return defaultMaterial;
|
||||
|
||||
}
|
||||
|
||||
String getColor() {
|
||||
|
||||
return color;
|
||||
|
||||
}
|
||||
|
||||
String getStatusLine() {
|
||||
|
||||
return statusLine;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
package net.trueog.gxui.progress;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import net.trueog.gxui.GUIBase;
|
||||
import net.trueog.gxui.GUIButton;
|
||||
import net.trueog.gxui.GUIItem;
|
||||
|
||||
// A read-only, layered inventory for groups of material-backed goals.
|
||||
public final class ProgressMenu extends GUIBase {
|
||||
|
||||
public static final int INVENTORY_SIZE = 54;
|
||||
public static final int CONTENT_PER_PAGE = 45;
|
||||
public static final int BACK_SLOT = 45;
|
||||
public static final int PREVIOUS_PAGE_SLOT = 47;
|
||||
public static final int CLOSE_SLOT = 49;
|
||||
public static final int NEXT_PAGE_SLOT = 51;
|
||||
public static final int INFORMATION_SLOT = 53;
|
||||
|
||||
private static final List<Integer> CONTENT_SLOTS = createContentSlots();
|
||||
private static final LegacyComponentSerializer LEGACY = LegacyComponentSerializer.legacyAmpersand();
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final String baseTitle;
|
||||
private final List<Section> sections;
|
||||
private final int sectionIndex;
|
||||
private final int page;
|
||||
|
||||
private ProgressMenu(JavaPlugin plugin, Player player, String baseTitle, List<Section> sections, int sectionIndex,
|
||||
int page)
|
||||
{
|
||||
|
||||
super(plugin, player, buildInventoryName(baseTitle, sections, sectionIndex, page), INVENTORY_SIZE, true);
|
||||
|
||||
this.plugin = plugin;
|
||||
this.baseTitle = baseTitle;
|
||||
this.sections = sections;
|
||||
this.sectionIndex = normalizeSectionIndex(sectionIndex, sections.size());
|
||||
this.page = normalizePage(page, getTotalPages(getVisibleItemCount(sections, this.sectionIndex)));
|
||||
|
||||
}
|
||||
|
||||
public static Builder builder(JavaPlugin plugin, Player player, String inventoryName) {
|
||||
|
||||
return new Builder(plugin, player, inventoryName);
|
||||
|
||||
}
|
||||
|
||||
// true when this menu is displaying one section's goals.
|
||||
public boolean isDetailView() {
|
||||
|
||||
return sectionIndex >= 0;
|
||||
|
||||
}
|
||||
|
||||
// Get the numeric page number (0 index).
|
||||
public int getPage() {
|
||||
|
||||
return page;
|
||||
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
|
||||
return getTotalPages(getVisibleItemCount(sections, sectionIndex));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupItems() {
|
||||
|
||||
if (isDetailView()) {
|
||||
|
||||
setupDetailItems();
|
||||
|
||||
} else {
|
||||
|
||||
setupIndexItems();
|
||||
|
||||
}
|
||||
|
||||
setupNavigationItems();
|
||||
|
||||
}
|
||||
|
||||
private void setupIndexItems() {
|
||||
|
||||
final int firstSection = page * CONTENT_PER_PAGE;
|
||||
final int lastSection = Math.min(firstSection + CONTENT_PER_PAGE, sections.size());
|
||||
for (int currentSection = firstSection; currentSection < lastSection; currentSection++) {
|
||||
|
||||
final int pageIndex = currentSection - firstSection;
|
||||
|
||||
addItem(CONTENT_SLOTS.get(pageIndex), buildSectionItem(currentSection));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void setupDetailItems() {
|
||||
|
||||
final Section section = sections.get(sectionIndex);
|
||||
final List<Goal> goals = section.goals();
|
||||
final int firstGoal = page * CONTENT_PER_PAGE;
|
||||
final int lastGoal = Math.min(firstGoal + CONTENT_PER_PAGE, goals.size());
|
||||
for (int currentGoal = firstGoal; currentGoal < lastGoal; currentGoal++) {
|
||||
|
||||
final int pageIndex = currentGoal - firstGoal;
|
||||
addItem(CONTENT_SLOTS.get(pageIndex), buildGoalItem(goals.get(currentGoal)));
|
||||
|
||||
}
|
||||
|
||||
if (!section.description().isEmpty()) {
|
||||
|
||||
addItem(INFORMATION_SLOT, buildInformationItem(section));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void setupNavigationItems() {
|
||||
|
||||
if (isDetailView()) {
|
||||
|
||||
addItem(BACK_SLOT, buildBackButton());
|
||||
|
||||
}
|
||||
|
||||
if (page > 0) {
|
||||
|
||||
addItem(PREVIOUS_PAGE_SLOT, buildPageButton("&ePrevious Page", page - 1));
|
||||
|
||||
}
|
||||
|
||||
addItem(CLOSE_SLOT, buildCloseButton());
|
||||
|
||||
if (page + 1 < getTotalPages()) {
|
||||
|
||||
addItem(NEXT_PAGE_SLOT, buildPageButton("&eNext Page", page + 1));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildSectionItem(int targetSection) {
|
||||
|
||||
final Section section = sections.get(targetSection);
|
||||
final State state = section.state();
|
||||
final GUIItem guiItem = new GUIItem(state.getMaterial(), 1, state.getColor() + section.name());
|
||||
|
||||
final List<String> lore = new ArrayList<>();
|
||||
lore.add(state.getStatusLine());
|
||||
|
||||
if (!section.description().isEmpty()) {
|
||||
|
||||
lore.add("");
|
||||
lore.addAll(section.description());
|
||||
|
||||
}
|
||||
|
||||
lore.add("");
|
||||
lore.add("&eClick to view requirements.");
|
||||
|
||||
guiItem.lore(toLoreComponents(lore));
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> openSection(targetSection, 0)));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildGoalItem(Goal goal) {
|
||||
|
||||
final String color = goal.complete() ? "&a" : "&c";
|
||||
final GUIItem guiItem = new GUIItem(goal.material(), 1, color + goal.name());
|
||||
|
||||
final List<String> lore = new ArrayList<>();
|
||||
lore.add(goal.complete() ? "&aCompleted" : "&cIncomplete");
|
||||
lore.add("");
|
||||
|
||||
if (goal.hasProgress()) {
|
||||
|
||||
lore.add("&7Progress: &f" + goal.current() + "&7/&f" + goal.target());
|
||||
|
||||
} else {
|
||||
|
||||
lore.add("&7Status: &f" + (goal.complete() ? "Complete" : "Incomplete"));
|
||||
|
||||
}
|
||||
|
||||
guiItem.lore(toLoreComponents(lore));
|
||||
guiItem.playErrorSound(false);
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildInformationItem(Section section) {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(Material.PAPER, 1, "&eInformation");
|
||||
|
||||
guiItem.lore(toLoreComponents(section.description()));
|
||||
guiItem.playErrorSound(false);
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildBackButton() {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(Material.ARROW, 1, "&eBack to Menu");
|
||||
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> openIndex(getIndexPage(sectionIndex))));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildCloseButton() {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(Material.BARRIER, 1, "&cClose");
|
||||
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> getPlayer().closeInventory()));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private GUIItem buildPageButton(String displayName, int targetPage) {
|
||||
|
||||
final GUIItem guiItem = new GUIItem(Material.ARROW, 1, displayName);
|
||||
|
||||
guiItem.lore(toLoreComponents(List.of("&7Page " + (targetPage + 1) + " of " + getTotalPages())));
|
||||
guiItem.playErrorSound(false);
|
||||
guiItem.button(createButton(() -> openCurrentLayer(targetPage)));
|
||||
|
||||
return guiItem;
|
||||
|
||||
}
|
||||
|
||||
private void openCurrentLayer(int targetPage) {
|
||||
|
||||
if (isDetailView()) {
|
||||
|
||||
openSection(sectionIndex, targetPage);
|
||||
|
||||
} else {
|
||||
|
||||
openIndex(targetPage);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void openIndex(int targetPage) {
|
||||
|
||||
new ProgressMenu(plugin, getPlayer(), baseTitle, sections, -1, targetPage).open(false);
|
||||
|
||||
}
|
||||
|
||||
private void openSection(int targetSection, int targetPage) {
|
||||
|
||||
new ProgressMenu(plugin, getPlayer(), baseTitle, sections, targetSection, targetPage).open(false);
|
||||
|
||||
}
|
||||
|
||||
private GUIButton createButton(Runnable clickHandler) {
|
||||
|
||||
return new GUIButton() {
|
||||
|
||||
@Override
|
||||
public boolean leftClick() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean leftClickShift() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rightClick() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean rightClickShift() {
|
||||
|
||||
return handleClick();
|
||||
|
||||
}
|
||||
|
||||
private boolean handleClick() {
|
||||
|
||||
clickHandler.run();
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private static List<TextComponent> toLoreComponents(List<String> lore) {
|
||||
|
||||
return lore.stream().map(ProgressMenu::toTextComponent).toList();
|
||||
|
||||
}
|
||||
|
||||
private static TextComponent toTextComponent(String line) {
|
||||
|
||||
final Component component = LEGACY.deserialize(line);
|
||||
|
||||
return (TextComponent) component;
|
||||
|
||||
}
|
||||
|
||||
private static String buildInventoryName(String baseTitle, List<Section> sections, int sectionIndex, int page) {
|
||||
|
||||
Objects.requireNonNull(baseTitle, "baseTitle");
|
||||
Objects.requireNonNull(sections, "sections");
|
||||
final int normalizedSection = normalizeSectionIndex(sectionIndex, sections.size());
|
||||
final String title = normalizedSection < 0 ? baseTitle : sections.get(normalizedSection).name();
|
||||
final int itemCount = getVisibleItemCount(sections, normalizedSection);
|
||||
final int totalPages = getTotalPages(itemCount);
|
||||
if (totalPages == 1) {
|
||||
|
||||
return title;
|
||||
|
||||
}
|
||||
|
||||
final int normalizedPage = normalizePage(page, totalPages);
|
||||
return title + " &7(" + (normalizedPage + 1) + "/" + totalPages + ")";
|
||||
|
||||
}
|
||||
|
||||
private static int getVisibleItemCount(List<Section> sections, int sectionIndex) {
|
||||
|
||||
return sectionIndex < 0 ? sections.size() : sections.get(sectionIndex).goals().size();
|
||||
|
||||
}
|
||||
|
||||
private static int getTotalPages(int itemCount) {
|
||||
|
||||
return Math.max(1, (int) Math.ceil((double) itemCount / CONTENT_PER_PAGE));
|
||||
|
||||
}
|
||||
|
||||
private static int normalizePage(int page, int totalPages) {
|
||||
|
||||
if (page < 0) {
|
||||
|
||||
return 0;
|
||||
|
||||
}
|
||||
|
||||
return page >= totalPages ? totalPages - 1 : page;
|
||||
|
||||
}
|
||||
|
||||
private static int normalizeSectionIndex(int sectionIndex, int sectionCount) {
|
||||
|
||||
return sectionIndex >= 0 && sectionIndex < sectionCount ? sectionIndex : -1;
|
||||
|
||||
}
|
||||
|
||||
private static int getIndexPage(int sectionIndex) {
|
||||
|
||||
return sectionIndex / CONTENT_PER_PAGE;
|
||||
|
||||
}
|
||||
|
||||
private static List<Integer> createContentSlots() {
|
||||
|
||||
final List<Integer> slots = new ArrayList<>(CONTENT_PER_PAGE);
|
||||
for (int row = 0; row < 5; row++) {
|
||||
|
||||
for (int column = 0; column < 9; column++) {
|
||||
|
||||
slots.add(row * 9 + column);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(slots);
|
||||
|
||||
}
|
||||
|
||||
// Section state and its fixed first-layer wool material.
|
||||
public enum State {
|
||||
|
||||
NOT_STARTED(Material.RED_WOOL, "&c", "&cNot Started"), IN_PROGRESS(Material.YELLOW_WOOL, "&e", "&eIn Progress"),
|
||||
COMPLETE(Material.LIME_WOOL, "&a", "&aCompleted");
|
||||
|
||||
private final Material material;
|
||||
private final String color;
|
||||
private final String statusLine;
|
||||
|
||||
State(Material material, String color, String statusLine) {
|
||||
|
||||
this.material = material;
|
||||
this.color = color;
|
||||
this.statusLine = statusLine;
|
||||
|
||||
}
|
||||
|
||||
private Material getMaterial() {
|
||||
|
||||
return material;
|
||||
|
||||
}
|
||||
|
||||
private String getColor() {
|
||||
|
||||
return color;
|
||||
|
||||
}
|
||||
|
||||
private String getStatusLine() {
|
||||
|
||||
return statusLine;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record Goal(Material material, String name, boolean complete, long current, long target,
|
||||
boolean hasProgress)
|
||||
{
|
||||
|
||||
private Goal(Material material, String name, boolean complete) {
|
||||
|
||||
this(material, name, complete, 0L, 0L, false);
|
||||
|
||||
}
|
||||
|
||||
private Goal(Material material, String name, long current, long target) {
|
||||
|
||||
this(material, name, current >= target, current, target, true);
|
||||
|
||||
}
|
||||
|
||||
private Goal {
|
||||
|
||||
Objects.requireNonNull(material, "material");
|
||||
Objects.requireNonNull(name, "name");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private record Section(String name, State state, List<Goal> goals, List<String> description) {
|
||||
|
||||
private Section {
|
||||
|
||||
Objects.requireNonNull(name, "name");
|
||||
Objects.requireNonNull(state, "state");
|
||||
|
||||
goals = List.copyOf(Objects.requireNonNull(goals, "goals"));
|
||||
description = List.copyOf(Objects.requireNonNull(description, "description"));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Builder for layered ProgressMenu.
|
||||
public static final class Builder {
|
||||
|
||||
private final JavaPlugin plugin;
|
||||
private final Player player;
|
||||
private final String title;
|
||||
private final List<Section> sections = new ArrayList<>();
|
||||
|
||||
private String pendingName;
|
||||
private State pendingState;
|
||||
private List<Goal> pendingGoals;
|
||||
private List<String> pendingDescription;
|
||||
|
||||
private Builder(JavaPlugin plugin, Player player, String title) {
|
||||
|
||||
this.plugin = Objects.requireNonNull(plugin, "plugin");
|
||||
this.player = Objects.requireNonNull(player, "player");
|
||||
this.title = Objects.requireNonNull(title, "title");
|
||||
|
||||
}
|
||||
|
||||
// Start a section. Its first-layer item is always the state's color of wool
|
||||
// (red, yellow, green).
|
||||
public Builder section(String name, State state) {
|
||||
|
||||
flushPending();
|
||||
|
||||
pendingName = Objects.requireNonNull(name, "name");
|
||||
pendingState = Objects.requireNonNull(state, "state");
|
||||
pendingGoals = new ArrayList<>();
|
||||
pendingDescription = new ArrayList<>();
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// A counter for which completion is derived from current >= target.
|
||||
public Builder progress(Material material, String name, long current, long target) {
|
||||
|
||||
requirePending();
|
||||
|
||||
pendingGoals.add(new Goal(material, name, current, target));
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
public Builder goal(Material material, String name, boolean complete) {
|
||||
|
||||
requirePending();
|
||||
|
||||
pendingGoals.add(new Goal(material, name, complete));
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
public Builder description(String... lines) {
|
||||
|
||||
requirePending();
|
||||
|
||||
Collections.addAll(pendingDescription, lines);
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
// Open the first-layer section.
|
||||
public ProgressMenu open() {
|
||||
|
||||
flushPending();
|
||||
|
||||
final ProgressMenu menu = new ProgressMenu(plugin, player, title, List.copyOf(sections), -1, 0);
|
||||
menu.open(false);
|
||||
|
||||
return menu;
|
||||
|
||||
}
|
||||
|
||||
private void requirePending() {
|
||||
|
||||
if (pendingName == null) {
|
||||
|
||||
throw new IllegalStateException("Call section(name, state) before adding goals or descriptions");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void flushPending() {
|
||||
|
||||
if (pendingName == null) {
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
sections.add(new Section(pendingName, pendingState, pendingGoals, pendingDescription));
|
||||
|
||||
pendingName = null;
|
||||
pendingState = null;
|
||||
pendingGoals = null;
|
||||
pendingDescription = null;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user