new progress GUI API

This commit is contained in:
NotAlexNoyle committed 2026-07-17 12:09:37 -04:00
1 parent e606c9d638
commit cc77691f2e
8 files changed
+625 -583

No files matched your search

+29 -15
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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;
}
}
}