|
@@ -0,0 +1,254 @@
|
|
|
|
|
+package me.lethunderhawk.bazaarflux.util.itemdesign;
|
|
|
|
|
+
|
|
|
|
|
+import net.kyori.adventure.text.Component;
|
|
|
|
|
+import net.kyori.adventure.text.ComponentBuilder;
|
|
|
|
|
+import net.kyori.adventure.text.TextComponent;
|
|
|
|
|
+import net.kyori.adventure.text.format.NamedTextColor;
|
|
|
|
|
+import net.kyori.adventure.text.format.TextDecoration;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.EnumSet;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+public final class LoreDesigner {
|
|
|
|
|
+
|
|
|
|
|
+ private LoreDesigner() {}
|
|
|
|
|
+ public static Component createSingle(String text) {
|
|
|
|
|
+ if (text == null || text.isBlank()) {
|
|
|
|
|
+ return Component.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ StyleState style = new StyleState();
|
|
|
|
|
+ ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
|
|
|
|
|
+
|
|
|
|
|
+ int i = 0;
|
|
|
|
|
+ while (i < text.length()) {
|
|
|
|
|
+ if (text.charAt(i) == '<') {
|
|
|
|
|
+ int end = text.indexOf('>', i);
|
|
|
|
|
+ if (end != -1) {
|
|
|
|
|
+ String token = text.substring(i + 1, end).toLowerCase();
|
|
|
|
|
+ applyToken(style, token);
|
|
|
|
|
+ i = end + 1;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ char c = text.charAt(i);
|
|
|
|
|
+ builder.append(Component.text(String.valueOf(c), style.toStyle()));
|
|
|
|
|
+ i++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return builder.build();
|
|
|
|
|
+ }
|
|
|
|
|
+ public static List<Component> createLore(String text, String widthReference) {
|
|
|
|
|
+ int maxLength = widthReference.length();
|
|
|
|
|
+ if (text == null || text.isBlank() || maxLength <= 0) {
|
|
|
|
|
+ return List.of();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<Component> lore = new ArrayList<>();
|
|
|
|
|
+
|
|
|
|
|
+ StyleState style = new StyleState();
|
|
|
|
|
+ ComponentBuilder<TextComponent, TextComponent.Builder> lineBuilder = Component.text();
|
|
|
|
|
+ int currentLength = 0;
|
|
|
|
|
+
|
|
|
|
|
+ String[] lines = text.split("<br>", -1);
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = 0; i < lines.length; i++) {
|
|
|
|
|
+
|
|
|
|
|
+ String[] wrapParts = lines[i].split(" ");
|
|
|
|
|
+
|
|
|
|
|
+ for (int p = 0; p < wrapParts.length; p++) {
|
|
|
|
|
+
|
|
|
|
|
+ ParsedWord parsed = parseWord(wrapParts[p], style);
|
|
|
|
|
+ appendWord(
|
|
|
|
|
+ lore,
|
|
|
|
|
+ parsed,
|
|
|
|
|
+ maxLength,
|
|
|
|
|
+ style,
|
|
|
|
|
+ Holder.of(lineBuilder, currentLength)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ currentLength = Holder.length;
|
|
|
|
|
+ lineBuilder = Holder.builder;
|
|
|
|
|
+
|
|
|
|
|
+ // add space between parts
|
|
|
|
|
+ if (p < wrapParts.length - 1) {
|
|
|
|
|
+ lineBuilder.append(Component.space());
|
|
|
|
|
+ currentLength++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ currentLength = Holder.length;
|
|
|
|
|
+ lineBuilder = Holder.builder;
|
|
|
|
|
+
|
|
|
|
|
+ // force line break if <br> occurred
|
|
|
|
|
+ if (i < lines.length - 1) {
|
|
|
|
|
+ lore.add(lineBuilder.build());
|
|
|
|
|
+ lineBuilder = Component.text().style(style.toStyle());
|
|
|
|
|
+ currentLength = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (currentLength > 0) {
|
|
|
|
|
+ lore.add(lineBuilder.build());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return lore;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // -------------------- Append Logic --------------------
|
|
|
|
|
+
|
|
|
|
|
+ private static void appendWord(
|
|
|
|
|
+ List<Component> lore,
|
|
|
|
|
+ ParsedWord parsed,
|
|
|
|
|
+ int maxLength,
|
|
|
|
|
+ StyleState style,
|
|
|
|
|
+ Holder holder
|
|
|
|
|
+ ) {
|
|
|
|
|
+ int wordLength = parsed.visibleLength();
|
|
|
|
|
+
|
|
|
|
|
+ if (holder.length > 0 && holder.length + 1 + wordLength > maxLength) {
|
|
|
|
|
+ lore.add(holder.builder.build());
|
|
|
|
|
+ holder.builder = Component.text().style(style.toStyle());
|
|
|
|
|
+ holder.length = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ holder.builder.append(parsed.component());
|
|
|
|
|
+ holder.length += wordLength;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // -------------------- Parsing --------------------
|
|
|
|
|
+
|
|
|
|
|
+ private static ParsedWord parseWord(String raw, StyleState style) {
|
|
|
|
|
+ ComponentBuilder<TextComponent, TextComponent.Builder> builder = Component.text();
|
|
|
|
|
+ int visibleLength = 0;
|
|
|
|
|
+
|
|
|
|
|
+ int i = 0;
|
|
|
|
|
+ while (i < raw.length()) {
|
|
|
|
|
+ if (raw.charAt(i) == '<') {
|
|
|
|
|
+ int end = raw.indexOf('>', i);
|
|
|
|
|
+ if (end != -1) {
|
|
|
|
|
+ String token = raw.substring(i + 1, end).toLowerCase();
|
|
|
|
|
+ applyToken(style, token);
|
|
|
|
|
+ i = end + 1;
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ char c = raw.charAt(i);
|
|
|
|
|
+ builder.append(Component.text(String.valueOf(c), style.toStyle()));
|
|
|
|
|
+ visibleLength++;
|
|
|
|
|
+ i++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new ParsedWord(builder.build(), visibleLength);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void applyToken(StyleState style, String token) {
|
|
|
|
|
+
|
|
|
|
|
+ if (token.startsWith("/")) {
|
|
|
|
|
+ String closing = token.substring(1);
|
|
|
|
|
+
|
|
|
|
|
+ switch (closing) {
|
|
|
|
|
+ case "bold" -> style.undecorate(TextDecoration.BOLD);
|
|
|
|
|
+ case "italic" -> style.undecorate(TextDecoration.ITALIC);
|
|
|
|
|
+ case "underlined" -> style.undecorate(TextDecoration.UNDERLINED);
|
|
|
|
|
+ case "strikethrough" -> style.undecorate(TextDecoration.STRIKETHROUGH);
|
|
|
|
|
+ case "obfuscated" -> style.undecorate(TextDecoration.OBFUSCATED);
|
|
|
|
|
+ default -> {
|
|
|
|
|
+ // closing color tag like </red>
|
|
|
|
|
+ if (NAMED_COLORS.containsKey(closing)) {
|
|
|
|
|
+ style.color(style.defaultColor);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ switch (token) {
|
|
|
|
|
+ case "reset" -> style.reset();
|
|
|
|
|
+
|
|
|
|
|
+ case "bold" -> style.decorate(TextDecoration.BOLD);
|
|
|
|
|
+ case "italic" -> style.decorate(TextDecoration.ITALIC);
|
|
|
|
|
+ case "underlined" -> style.decorate(TextDecoration.UNDERLINED);
|
|
|
|
|
+ case "strikethrough" -> style.decorate(TextDecoration.STRIKETHROUGH);
|
|
|
|
|
+ case "obfuscated" -> style.decorate(TextDecoration.OBFUSCATED);
|
|
|
|
|
+
|
|
|
|
|
+ default -> {
|
|
|
|
|
+ NamedTextColor color = NAMED_COLORS.get(token);
|
|
|
|
|
+ if (color != null) {
|
|
|
|
|
+ style.color(color);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // -------------------- Helpers --------------------
|
|
|
|
|
+
|
|
|
|
|
+ private static final Map<String, NamedTextColor> NAMED_COLORS = Map.ofEntries(
|
|
|
|
|
+ Map.entry("black", NamedTextColor.BLACK),
|
|
|
|
|
+ Map.entry("dark_blue", NamedTextColor.DARK_BLUE),
|
|
|
|
|
+ Map.entry("dark_green", NamedTextColor.DARK_GREEN),
|
|
|
|
|
+ Map.entry("dark_aqua", NamedTextColor.DARK_AQUA),
|
|
|
|
|
+ Map.entry("dark_red", NamedTextColor.DARK_RED),
|
|
|
|
|
+ Map.entry("dark_purple", NamedTextColor.DARK_PURPLE),
|
|
|
|
|
+ Map.entry("gold", NamedTextColor.GOLD),
|
|
|
|
|
+ Map.entry("gray", NamedTextColor.GRAY),
|
|
|
|
|
+ Map.entry("dark_gray", NamedTextColor.DARK_GRAY),
|
|
|
|
|
+ Map.entry("blue", NamedTextColor.BLUE),
|
|
|
|
|
+ Map.entry("green", NamedTextColor.GREEN),
|
|
|
|
|
+ Map.entry("aqua", NamedTextColor.AQUA),
|
|
|
|
|
+ Map.entry("red", NamedTextColor.RED),
|
|
|
|
|
+ Map.entry("light_purple", NamedTextColor.LIGHT_PURPLE),
|
|
|
|
|
+ Map.entry("yellow", NamedTextColor.YELLOW),
|
|
|
|
|
+ Map.entry("white", NamedTextColor.WHITE)
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ private record ParsedWord(Component component, int visibleLength) {}
|
|
|
|
|
+
|
|
|
|
|
+ private static final class StyleState {
|
|
|
|
|
+ private final NamedTextColor defaultColor = NamedTextColor.GRAY;
|
|
|
|
|
+
|
|
|
|
|
+ private NamedTextColor color = defaultColor;
|
|
|
|
|
+ private final EnumSet<TextDecoration> decorations = EnumSet.noneOf(TextDecoration.class);
|
|
|
|
|
+
|
|
|
|
|
+ void color(NamedTextColor color) {
|
|
|
|
|
+ this.color = color;
|
|
|
|
|
+ }
|
|
|
|
|
+ void undecorate(TextDecoration decoration) {
|
|
|
|
|
+ decorations.remove(decoration);
|
|
|
|
|
+ }
|
|
|
|
|
+ void decorate(TextDecoration decoration) {
|
|
|
|
|
+ decorations.add(decoration);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void reset() {
|
|
|
|
|
+ color = defaultColor;
|
|
|
|
|
+ decorations.clear();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ net.kyori.adventure.text.format.Style toStyle() {
|
|
|
|
|
+ net.kyori.adventure.text.format.Style.Builder style =
|
|
|
|
|
+ net.kyori.adventure.text.format.Style.style();
|
|
|
|
|
+ if (color != null) style.color(color);
|
|
|
|
|
+ for (TextDecoration d : decorations) style.decorate(d);
|
|
|
|
|
+ return style.build();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Mutable holder to avoid excessive object creation
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final class Holder {
|
|
|
|
|
+ static ComponentBuilder builder;
|
|
|
|
|
+ static int length;
|
|
|
|
|
+
|
|
|
|
|
+ static Holder of(ComponentBuilder b, int l) {
|
|
|
|
|
+ builder = b;
|
|
|
|
|
+ length = l;
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|