diff options
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock')
8 files changed, 1706 insertions, 203 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java deleted file mode 100644 index 3456d1ad..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/FancyStatusBars.java +++ /dev/null @@ -1,191 +0,0 @@ -package de.hysky.skyblocker.skyblock; - -import de.hysky.skyblocker.SkyblockerMod; -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.utils.Utils; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.font.TextRenderer; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.util.Identifier; - -public class FancyStatusBars { - private static final Identifier BARS = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/bars.png"); - - private final MinecraftClient client = MinecraftClient.getInstance(); - private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker; - - private final StatusBar[] bars = new StatusBar[]{ - new StatusBar(0, 16733525, 2), // Health Bar - new StatusBar(1, 5636095, 2), // Intelligence Bar - new StatusBar(2, 12106180, 1), // Defence Bar - new StatusBar(3, 8453920, 1), // Experience Bar - }; - - // Positions to show the bars - // 0: Hotbar Layer 1, 1: Hotbar Layer 2, 2: Right of hotbar - // Anything outside the set values hides the bar - private final int[] anchorsX = new int[3]; - private final int[] anchorsY = new int[3]; - - public FancyStatusBars() { - moveBar(0, 0); - moveBar(1, 0); - moveBar(2, 0); - moveBar(3, 0); - } - - private int fill(int value, int max) { - return (100 * value) / max; - } - - public boolean render(DrawContext context, int scaledWidth, int scaledHeight) { - var player = client.player; - if (!SkyblockerConfigManager.get().general.bars.enableBars || player == null || Utils.isInTheRift()) - return false; - anchorsX[0] = scaledWidth / 2 - 91; - anchorsY[0] = scaledHeight - 33; - anchorsX[1] = anchorsX[0]; - anchorsY[1] = anchorsY[0] - 10; - anchorsX[2] = (scaledWidth / 2 + 91) + 2; - anchorsY[2] = scaledHeight - 16; - - bars[0].update(statusBarTracker.getHealth()); - bars[1].update(statusBarTracker.getMana()); - int def = statusBarTracker.getDefense(); - bars[2].fill[0] = fill(def, def + 100); - bars[2].text = def; - bars[3].fill[0] = (int) (100 * player.experienceProgress); - bars[3].text = player.experienceLevel; - - // Update positions of bars from config - for (int i = 0; i < 4; i++) { - int configAnchorNum = switch (i) { - case 0 -> SkyblockerConfigManager.get().general.bars.barPositions.healthBarPosition.toInt(); - case 1 -> SkyblockerConfigManager.get().general.bars.barPositions.manaBarPosition.toInt(); - case 2 -> SkyblockerConfigManager.get().general.bars.barPositions.defenceBarPosition.toInt(); - case 3 -> SkyblockerConfigManager.get().general.bars.barPositions.experienceBarPosition.toInt(); - default -> 0; - }; - - if (bars[i].anchorNum != configAnchorNum) - moveBar(i, configAnchorNum); - } - - for (var bar : bars) { - bar.draw(context); - } - for (var bar : bars) { - bar.drawText(context); - } - return true; - } - - public void moveBar(int bar, int location) { - // Set the bar to the new anchor - bars[bar].anchorNum = location; - - // Count how many bars are in each location - int layer1Count = 0, layer2Count = 0; - for (int i = 0; i < 4; i++) { - switch (bars[i].anchorNum) { - case 0 -> layer1Count++; - case 1 -> layer2Count++; - } - } - - // Set the bars width and offsetX according to their anchor and how many bars are on that layer - int adjustedLayer1Count = 0, adjustedLayer2Count = 0, adjustedRightCount = 0; - for (int i = 0; i < 4; i++) { - switch (bars[i].anchorNum) { - case 0 -> { - bars[i].bar_width = (172 - ((layer1Count - 1) * 11)) / layer1Count; - bars[i].offsetX = adjustedLayer1Count * (bars[i].bar_width + 11 + (layer1Count == 3 ? 0 : 1)); - adjustedLayer1Count++; - } - case 1 -> { - bars[i].bar_width = (172 - ((layer2Count - 1) * 11)) / layer2Count; - bars[i].offsetX = adjustedLayer2Count * (bars[i].bar_width + 11 + (layer2Count == 3 ? 0 : 1)); - adjustedLayer2Count++; - } - case 2 -> { - bars[i].bar_width = 50; - bars[i].offsetX = adjustedRightCount * (50 + 11); - adjustedRightCount++; - } - } - } - } - - private class StatusBar { - public final int[] fill; - public int offsetX; - private final int v; - private final int text_color; - public int anchorNum; - public int bar_width; - public Object text; - - private StatusBar(int i, int textColor, int fillNum) { - this.v = i * 9; - this.text_color = textColor; - this.fill = new int[fillNum]; - this.fill[0] = 100; - this.anchorNum = 0; - this.text = ""; - } - - public void update(StatusBarTracker.Resource resource) { - int max = resource.max(); - int val = resource.value(); - this.fill[0] = fill(val, max); - this.fill[1] = fill(resource.overflow(), max); - this.text = val; - } - - public void draw(DrawContext context) { - // Dont draw if anchorNum is outside of range - if (anchorNum < 0 || anchorNum > 2) return; - - // Draw the icon for the bar - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX, anchorsY[anchorNum], 0, v, 9, 9); - - // Draw the background for the bar - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10, anchorsY[anchorNum], 10, v, 2, 9); - for (int i = 2; i < bar_width - 2; i += 58) { - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + i, anchorsY[anchorNum], 12, v, Math.min(58, bar_width - 2 - i), 9); - } - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 10 + bar_width - 2, anchorsY[anchorNum], 70, v, 2, 9); - - // Draw the filled part of the bar - for (int i = 0; i < fill.length; i++) { - int fill_width = this.fill[i] * (bar_width - 2) / 100; - if (fill_width >= 1) { - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11, anchorsY[anchorNum], 72 + i * 60, v, 1, 9); - } - for (int j = 1; j < fill_width - 1; j += 58) { - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + j, anchorsY[anchorNum], 73 + i * 60, v, Math.min(58, fill_width - 1 - j), 9); - } - if (fill_width == bar_width - 2) { - context.drawTexture(BARS, anchorsX[anchorNum] + offsetX + 11 + fill_width - 1, anchorsY[anchorNum], 131 + i * 60, v, 1, 9); - } - } - } - - public void drawText(DrawContext context) { - // Dont draw if anchorNum is outside of range - if (anchorNum < 0 || anchorNum > 2) return; - - TextRenderer textRenderer = client.textRenderer; - String text = this.text.toString(); - int x = anchorsX[anchorNum] + this.offsetX + 11 + (bar_width - textRenderer.getWidth(text)) / 2; - int y = anchorsY[anchorNum] - 3; - - final int[] offsets = new int[]{-1, 1}; - for (int i : offsets) { - context.drawText(textRenderer, text, x + i, y, 0, false); - context.drawText(textRenderer, text, x, y + i, 0, false); - } - context.drawText(textRenderer, text, x, y, text_color, false); - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java index 6b90b86c..9d460803 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/auction/EditBidPopup.java @@ -34,18 +34,7 @@ public class EditBidPopup extends AbstractPopupScreen { super.init(); layout = DirectionalLayoutWidget.vertical(); layout.spacing(8).getMainPositioner().alignHorizontalCenter(); - textFieldWidget = new TextFieldWidget(textRenderer, 120, 15, Text.empty()) { - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - if (!super.keyPressed(keyCode, scanCode, modifiers)) { - if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) { - done(null); - return true; - } - } else return true; - return false; - } - }; + textFieldWidget = new EnterConfirmTextFieldWidget(textRenderer, 120, 15, Text.empty(), () -> done(null)); textFieldWidget.setTextPredicate(this::isStringGood); layout.add(new TextWidget(Text.literal("- Set Bid -").fillStyle(Style.EMPTY.withBold(true)), textRenderer)); layout.add(textFieldWidget); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java new file mode 100644 index 00000000..a72b955b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/BarPositioner.java @@ -0,0 +1,308 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import net.minecraft.client.gui.ScreenPos; +import net.minecraft.client.gui.ScreenRect; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class BarPositioner { + + private final Map<BarAnchor, LinkedList<LinkedList<StatusBar>>> map = new HashMap<>(BarAnchor.values().length); + + public BarPositioner() { + for (BarAnchor value : BarAnchor.values()) { + map.put(value, new LinkedList<>()); + } + } + + + public int getRowCount(@NotNull BarAnchor barAnchor) { + return map.get(barAnchor).size(); + } + + /** + * Adds a row to the end of an anchor + * + * @param barAnchor the anchor + */ + public void addRow(@NotNull BarAnchor barAnchor) { + map.get(barAnchor).add(new LinkedList<>()); + } + + /** + * Adds a row at the specified index + * + * @param barAnchor the anchor + * @param row row index + */ + public void addRow(@NotNull BarAnchor barAnchor, int row) { + map.get(barAnchor).add(row, new LinkedList<>()); + } + + /** + * adds a bar to the end of a row + * + * @param barAnchor the anchor + * @param row the row + * @param bar the bar to add + */ + public void addBar(@NotNull BarAnchor barAnchor, int row, StatusBar bar) { + LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row); + statusBars.add(bar); + bar.gridY = row; + bar.gridX = statusBars.lastIndexOf(bar); // optimization baby, start with the end! + bar.anchor = barAnchor; + } + + /** + * adds a bar to the specified x in a row + * + * @param barAnchor the anchor + * @param row the row + * @param x the index in the row + * @param bar the bar to add + */ + public void addBar(@NotNull BarAnchor barAnchor, int row, int x, StatusBar bar) { + LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row); + statusBars.add(x, bar); + bar.gridY = row; + bar.gridX = statusBars.indexOf(bar); + bar.anchor = barAnchor; + } + + /** + * removes the specified bar at x on the row. If it's row is empty after being removed, the row will be auto removed + * + * @param barAnchor the anchor + * @param row dah row + * @param x dah x + */ + public void removeBar(@NotNull BarAnchor barAnchor, int row, int x) { + LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row); + StatusBar remove = statusBars.remove(x); + remove.anchor = null; + for (int i = x; i < statusBars.size(); i++) { + statusBars.get(i).gridX--; + } + if (statusBars.isEmpty()) removeRow(barAnchor, row); + } + + /** + * removes the specified bar on the row. If it's row is empty after being removed, the row will be auto removed + * + * @param barAnchor the anchor + * @param row dah row + * @param bar dah bar + */ + public void removeBar(@NotNull BarAnchor barAnchor, int row, StatusBar bar) { + LinkedList<StatusBar> barRow = map.get(barAnchor).get(row); + int x = barRow.indexOf(bar); + if (x < 0) return; // probably a bad idea + + barRow.remove(bar); + bar.anchor = null; + for (int i = x; i < barRow.size(); i++) { + barRow.get(i).gridX--; + } + if (barRow.isEmpty()) removeRow(barAnchor, row); + } + + /** + * row must be empty + * + * @param barAnchor the anchor + * @param row the row to remove + */ + public void removeRow(@NotNull BarAnchor barAnchor, int row) { + LinkedList<StatusBar> barRow = map.get(barAnchor).get(row); + if (!barRow.isEmpty()) + throw new IllegalStateException("Can't remove a non-empty row (" + barAnchor + "," + row + ")"); + map.get(barAnchor).remove(row); + for (int i = row; i < map.get(barAnchor).size(); i++) { + for (StatusBar statusBar : map.get(barAnchor).get(i)) { + statusBar.gridY--; + } + } + } + + + public LinkedList<StatusBar> getRow(@NotNull BarAnchor barAnchor, int row) { + return map.get(barAnchor).get(row); + } + + public StatusBar getBar(@NotNull BarAnchor barAnchor, int row, int x) { + return map.get(barAnchor).get(row).get(x); + } + + public boolean hasNeighbor(@NotNull BarAnchor barAnchor, int row, int x, boolean right) { + LinkedList<StatusBar> statusBars = map.get(barAnchor).get(row); + if (barAnchor.isRight()) { + return (right && x < statusBars.size() - 1) || (!right && x > 0); + } else { + return (right && x > 0) || (!right && x < statusBars.size() - 1); + } + } + + + public enum BarAnchor { + HOTBAR_LEFT(true, false, + (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 - 91 - 2, scaledHeight - 5), + SizeRule.freeSize(25, 2, 6)), + + HOTBAR_RIGHT(true, true, + (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 + 91 + 2, scaledHeight - 5), + SizeRule.freeSize(25, 2, 6)), + + HOTBAR_TOP(true, true, + (scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth / 2 - 91, scaledHeight - 23), + SizeRule.targetSize(12, 182, 2), + anchorPosition -> new ScreenRect(anchorPosition.x(), anchorPosition.y() - 20, 182, 20)), + + SCREEN_TOP_LEFT(false, true, + ((scaledWidth, scaledHeight) -> new ScreenPos(5, 5)), + SizeRule.freeSize(25, 2, 6) + ), + SCREEN_TOP_RIGHT(false, false, + ((scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth - 5, 5)), + SizeRule.freeSize(25, 2, 6) + ), + SCREEN_BOTTOM_LEFT(true, true, + ((scaledWidth, scaledHeight) -> new ScreenPos(5, scaledHeight - 5)), + SizeRule.freeSize(25, 2, 6) + ), + SCREEN_BOTTOM_RIGHT(true, false, + ((scaledWidth, scaledHeight) -> new ScreenPos(scaledWidth - 5, scaledHeight - 5)), + SizeRule.freeSize(25, 2, 6) + ); + + private final AnchorPositionProvider positionProvider; + private final AnchorHitboxProvider hitboxProvider; + private final boolean up; + private final boolean right; + private final SizeRule sizeRule; + + /** + * @param up whether the rows stack towards the top of the screen from the anchor (false is bottom) + * @param right whether the bars are line up towards the right of the screen from the anchor (false is left) + * @param positionProvider provides the position of the anchor for a give screen size + * @param sizeRule the rule the bars should follow. See {@link SizeRule} + * @param hitboxProvider provides the hitbox for when the anchor has no bars for the config screen + */ + BarAnchor(boolean up, boolean right, AnchorPositionProvider positionProvider, SizeRule sizeRule, AnchorHitboxProvider hitboxProvider) { + this.positionProvider = positionProvider; + this.up = up; + this.right = right; + this.hitboxProvider = hitboxProvider; + this.sizeRule = sizeRule; + } + + BarAnchor(boolean up, boolean right, AnchorPositionProvider positionProvider, SizeRule sizeRule) { + this(up, right, positionProvider, sizeRule, + anchorPosition -> new ScreenRect(anchorPosition.x() - (right ? 0 : 20), anchorPosition.y() - (up ? 20 : 0), 20, 20)); + } + + public ScreenPos getAnchorPosition(int scaledWidth, int scaledHeight) { + return positionProvider.getPosition(scaledWidth, scaledHeight); + } + + public ScreenRect getAnchorHitbox(ScreenPos anchorPosition) { + return hitboxProvider.getHitbox(anchorPosition); + } + + /** + * whether the rows stack towards the top of the screen from the anchor (false is bottom) + * + * @return true if towards the top, false otherwise + */ + public boolean isUp() { + return up; + } + + /** + * whether the bars are line up towards the right of the screen from the anchor (false is left) + * + * @return true if towards the right, false otherwise + */ + public boolean isRight() { + return right; + } + + public SizeRule getSizeRule() { + return sizeRule; + } + + private static final List<BarAnchor> cached = List.of(values()); + + /** + * cached version of {@link BarAnchor#values()} + * + * @return the list of anchors + */ + public static List<BarAnchor> allAnchors() { + return cached; + } + } + + /** + * The rules the bars on an anchor should follow + * + * @param isTargetSize whether the bars went to fit to a target width + * @param targetSize the size of all the bars on a row should add up to this (target size) + * @param totalWidth the total width taken by all the bars on the row (target size) + * @param widthPerSize the width of each size "unit" (free size) + * @param minSize the minimum (free and target size) + * @param maxSize the maximum (free and target size, THIS SHOULD BE THE SAME AS {@code targetSize} FOR {@code isTargetSize = true}) + */ + public record SizeRule(boolean isTargetSize, int targetSize, int totalWidth, int widthPerSize, int minSize, int maxSize) { + public static SizeRule freeSize(int widthPerSize, int minSize, int maxSize) { + return new SizeRule(false, -1, -1, widthPerSize, minSize, maxSize); + } + + public static SizeRule targetSize(int targetSize, int totalWidth, int minSize) { + return new SizeRule(true, targetSize, totalWidth, -1, minSize, targetSize); + } + } + + /** + * A record representing a snapshot of a bar's position + * + * @param barAnchor + * @param x + * @param y the row + */ + public record BarLocation(@Nullable BarAnchor barAnchor, int x, int y) { + + public static final BarLocation NULL = new BarLocation(null, -1, -1); + + public static BarLocation of(StatusBar bar) { + return new BarLocation(bar.anchor, bar.gridX, bar.gridY); + } + + public boolean equals(BarAnchor barAnchor, int x, int y) { + return x == this.x && y == this.y && barAnchor == this.barAnchor; + } + } + + /** + * provides the position of the anchor for a give screen size + */ + @FunctionalInterface + interface AnchorPositionProvider { + + ScreenPos getPosition(int scaledWidth, int scaledHeight); + } + + @FunctionalInterface + interface AnchorHitboxProvider { + + /** + * The hitbox, as in how large the area of "snapping" is if there are no bars on this anchor + * + * @param anchorPosition the position of the anchor + * @return the rectangle that represents the hitbox + */ + ScreenRect getHitbox(ScreenPos anchorPosition); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java new file mode 100644 index 00000000..64e79bab --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarColorPopup.java @@ -0,0 +1,117 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.*; +import net.minecraft.text.Style; +import net.minecraft.text.Text; + +import java.awt.*; +import java.util.List; +import java.util.function.Consumer; + +public class EditBarColorPopup extends AbstractPopupScreen { + + private final Consumer<Color> setColor; + + private DirectionalLayoutWidget layout = DirectionalLayoutWidget.vertical(); + private BasicColorSelector colorSelector; + + protected EditBarColorPopup(Text title, Screen backgroundScreen, Consumer<Color> setColor) { + super(title, backgroundScreen); + this.setColor = setColor; + } + + @Override + protected void init() { + super.init(); + layout = DirectionalLayoutWidget.vertical(); + layout.spacing(8).getMainPositioner().alignHorizontalCenter(); + layout.add(new TextWidget(title.copy().fillStyle(Style.EMPTY.withBold(true)), MinecraftClient.getInstance().textRenderer)); + colorSelector = new BasicColorSelector(0, 0, 150, () -> done(null)); + layout.add(colorSelector); + + DirectionalLayoutWidget horizontal = DirectionalLayoutWidget.horizontal(); + ButtonWidget buttonWidget = ButtonWidget.builder(Text.literal("Cancel"), button -> close()).width(80).build(); + horizontal.add(buttonWidget); + horizontal.add(ButtonWidget.builder(Text.literal("Done"), this::done).width(80).build()); + + layout.add(horizontal); + layout.forEachChild(this::addDrawableChild); + this.layout.refreshPositions(); + SimplePositioningWidget.setPos(layout, this.getNavigationFocus()); + } + + private void done(Object object) { + if (colorSelector.validColor) setColor.accept(new Color(colorSelector.getColor())); + close(); + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + super.renderBackground(context, mouseX, mouseY, delta); + drawPopupBackground(context, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight()); + } + + private static class BasicColorSelector extends ContainerWidget { + + private final EnterConfirmTextFieldWidget textFieldWidget; + + public BasicColorSelector(int x, int y, int width, Runnable onEnter) { + super(x, y, width, 15, Text.literal("edit color")); + textFieldWidget = new EnterConfirmTextFieldWidget(MinecraftClient.getInstance().textRenderer, getX() + 16, getY(), width - 16, 15, Text.empty(), onEnter); + textFieldWidget.setChangedListener(this::onTextChange); + textFieldWidget.setTextPredicate(s -> s.length() <= 6); + } + + @Override + public List<? extends Element> children() { + return List.of(textFieldWidget); + } + + public int getColor() { + return color; + } + + private int color = 0xFF000000; + private boolean validColor = false; + + private void onTextChange(String text) { + try { + color = Integer.parseInt(text, 16) | 0xFF000000; + validColor = true; + } catch (NumberFormatException e) { + color = 0; + validColor = false; + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawBorder(getX(), getY(), 15, 15, validColor ? -1 : 0xFFDD0000); + context.fill(getX() + 1, getY() + 1, getX() + 14, getY() + 14, color); + textFieldWidget.renderWidget(context, mouseX, mouseY, delta); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + public void setX(int x) { + super.setX(x); + textFieldWidget.setX(getX() + 16); + } + + @Override + public void setY(int y) { + super.setY(y); + textFieldWidget.setY(getY()); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java new file mode 100644 index 00000000..54af7a03 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/EditBarWidget.java @@ -0,0 +1,274 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.TooltipBackgroundRenderer; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ContainerWidget; +import net.minecraft.client.gui.widget.TextWidget; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Formatting; + +import java.awt.*; +import java.util.List; +import java.util.function.Consumer; + +public class EditBarWidget extends ContainerWidget { + + private final EnumCyclingOption<StatusBar.IconPosition> iconOption; + private final BooleanOption booleanOption; + + private final ColorOption color1; + private final ColorOption color2; + private final ColorOption textColor; + private final TextWidget nameWidget; + + private int contentsWidth = 0; + + public EditBarWidget(int x, int y, Screen parent) { + super(x, y, 100, 66, Text.literal("Edit bar")); + + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + + nameWidget = new TextWidget(Text.empty(), textRenderer); + + MutableText translatable = Text.translatable("skyblocker.bars.config.icon"); + contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + textRenderer.getWidth("RIGHT") + 10); + iconOption = new EnumCyclingOption<>(0, 11, getWidth(), translatable, StatusBar.IconPosition.class); + + translatable = Text.translatable("skyblocker.bars.config.showValue"); + contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10); + booleanOption = new BooleanOption(0, 22, getWidth(), translatable); + + // COLO(u)RS + translatable = Text.translatable("skyblocker.bars.config.mainColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10); + color1 = new ColorOption(0, 33, getWidth(), translatable, parent); + + translatable = Text.translatable("skyblocker.bars.config.overflowColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10); + color2 = new ColorOption(0, 44, getWidth(), translatable, parent); + + translatable = Text.translatable("skyblocker.bars.config.textColor"); + contentsWidth = Math.max(contentsWidth, textRenderer.getWidth(translatable) + 9 + 10); + textColor = new ColorOption(0, 55, getWidth(), translatable, parent); + + setWidth(contentsWidth); + } + + @Override + public List<? extends Element> children() { + return List.of(iconOption, booleanOption, color1, color2, textColor); + } + + public int insideMouseX = 0; + public int insideMouseY = 0; + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + if (isHovered()) { + insideMouseX = mouseX; + insideMouseY = mouseY; + } else { + int i = mouseX - insideMouseX; + int j = mouseY - insideMouseY; + if (i * i + j * j > 30 * 30) visible = false; + } + TooltipBackgroundRenderer.render(context, getX(), getY(), getWidth(), getHeight(), 0); + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(getX(), getY(), 0); + nameWidget.render(context, mouseX, mouseY, delta); + iconOption.renderWidget(context, mouseX - getX(), mouseY - getY(), delta); + booleanOption.renderWidget(context, mouseX - getX(), mouseY - getY(), delta); + color1.renderWidget(context, mouseX - getX(), mouseY - getY(), delta); + color2.renderWidget(context, mouseX - getX(), mouseY - getY(), delta); + textColor.renderWidget(context, mouseX - getX(), mouseY - getY(), delta); + matrices.pop(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!visible) return false; + if (!isHovered()) visible = false; + return super.mouseClicked(mouseX - getX(), mouseY - getY(), button); + } + + public void setStatusBar(StatusBar statusBar) { + iconOption.setCurrent(statusBar.getIconPosition()); + iconOption.setOnChange(statusBar::setIconPosition); + booleanOption.setCurrent(statusBar.showText()); + booleanOption.setOnChange(statusBar::setShowText); + + color1.setCurrent(statusBar.getColors()[0].getRGB()); + color1.setOnChange(color -> statusBar.getColors()[0] = color); + + color2.active = statusBar.hasOverflow(); + if (color2.active) { + color2.setCurrent(statusBar.getColors()[1].getRGB()); + color2.setOnChange(color -> statusBar.getColors()[1] = color); + } + + if (statusBar.getTextColor() != null) { + textColor.setCurrent(statusBar.getTextColor().getRGB()); + } + textColor.setOnChange(statusBar::setTextColor); + + MutableText formatted = statusBar.getName().copy().formatted(Formatting.BOLD); + nameWidget.setMessage(formatted); + setWidth(Math.max(MinecraftClient.getInstance().textRenderer.getWidth(formatted), contentsWidth)); + } + + @Override + public void setWidth(int width) { + super.setWidth(width); + iconOption.setWidth(width); + booleanOption.setWidth(width); + color1.setWidth(width); + color2.setWidth(width); + textColor.setWidth(width); + nameWidget.setWidth(width); + + } + + public static class EnumCyclingOption<T extends Enum<T>> extends ClickableWidget { + + private T current; + private final T[] values; + private Consumer<T> onChange = null; + + public EnumCyclingOption(int x, int y, int width, Text message, Class<T> enumClass) { + super(x, y, width, 11, message); + values = enumClass.getEnumConstants(); + current = values[0]; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, -1, true); + String string = current.toString(); + context.drawText(textRenderer, string, getRight() - textRenderer.getWidth(string) - 1, getY() + 1, -1, true); + } + + public void setCurrent(T current) { + this.current = current; + } + + @Override + public void onClick(double mouseX, double mouseY) { + current = values[(current.ordinal() + 1) % values.length]; + if (onChange != null) onChange.accept(current); + super.onClick(mouseX, mouseY); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + } + + public void setOnChange(Consumer<T> onChange) { + this.onChange = onChange; + } + } + + public static class BooleanOption extends ClickableWidget { + + private boolean current = false; + private Consumer<Boolean> onChange = null; + + public BooleanOption(int x, int y, int width, Text message) { + super(x, y, width, 11, message); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, -1, true); + context.drawBorder(getRight() - 10, getY() + 1, 9, 9, -1); + if (current) context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, -1); + } + + @Override + public void onClick(double mouseX, double mouseY) { + current = !current; + if (onChange != null) onChange.accept(current); + super.onClick(mouseX, mouseY); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + } + + public void setCurrent(boolean current) { + this.current = current; + } + + public void setOnChange(Consumer<Boolean> onChange) { + this.onChange = onChange; + } + } + + public static class ColorOption extends ClickableWidget { + + public void setCurrent(int current) { + this.current = current; + } + + private int current = 0; + private Consumer<Color> onChange = null; + private final Screen parent; + + public ColorOption(int x, int y, int width, Text message, Screen parent) { + super(x, y, width, 11, message); + this.parent = parent; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + if (isMouseOver(mouseX, mouseY)) { + context.fill(getX(), getY(), getRight(), getBottom(), 0x20FFFFFF); + } + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + context.drawText(textRenderer, getMessage(), getX() + 1, getY() + 1, active ? -1 : Colors.GRAY, true); + context.drawBorder(getRight() - 10, getY() + 1, 9, 9, active ? -1 : Colors.GRAY); + context.fill(getRight() - 8, getY() + 3, getRight() - 3, getY() + 8, active ? current : Colors.GRAY); + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + MinecraftClient.getInstance().setScreen(new EditBarColorPopup(Text.literal("Edit ").append(getMessage()), parent, this::set)); + } + + private void set(Color color) { + current = color.getRGB(); + if (onChange != null) onChange.accept(color); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public void setOnChange(Consumer<Color> onChange) { + this.onChange = onChange; + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java new file mode 100644 index 00000000..a960c9af --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/FancyStatusBars.java @@ -0,0 +1,288 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfig; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.StatusBarTracker; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.ScreenPos; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.lwjgl.glfw.GLFW; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.*; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class FancyStatusBars { + private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("status_bars.json"); + private static final Logger LOGGER = LoggerFactory.getLogger(FancyStatusBars.class); + + private final MinecraftClient client = MinecraftClient.getInstance(); + private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker; + + public static BarPositioner barPositioner = new BarPositioner(); + public static Map<String, StatusBar> statusBars = new HashMap<>(); + + public static void init() { + statusBars.put("health", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/health"), + new Color[]{new Color(255, 0, 0), new Color(255, 220, 0)}, + true, new Color(255, 85, 85), Text.translatable("skyblocker.bars.config.health"))); + statusBars.put("intelligence", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/intelligence"), + new Color[]{new Color(0, 255, 255), new Color(180, 0, 255)}, + true, new Color(85, 255, 255), Text.translatable("skyblocker.bars.config.intelligence"))); + statusBars.put("defense", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/defense"), + new Color[]{new Color(255, 255, 255)}, + false, new Color(185, 185, 185), Text.translatable("skyblocker.bars.config.defense"))); + statusBars.put("experience", new StatusBar(new Identifier(SkyblockerMod.NAMESPACE, "bars/icons/experience"), + new Color[]{new Color(100, 230, 70)}, + false, new Color(128, 255, 32), Text.translatable("skyblocker.bars.config.experience"))); + + // Fetch from old status bar config + int[] counts = new int[3]; // counts for RIGHT, LAYER1, LAYER2 + StatusBar health = statusBars.get("health"); + SkyblockerConfig.OldBarPositions barPositions = SkyblockerConfigManager.get().general.bars.barPositions; + loadOldBarPosition(health, counts, barPositions.healthBarPosition); + StatusBar intelligence = statusBars.get("intelligence"); + loadOldBarPosition(intelligence, counts, barPositions.manaBarPosition); + StatusBar defense = statusBars.get("defense"); + loadOldBarPosition(defense, counts, barPositions.defenceBarPosition); + StatusBar experience = statusBars.get("experience"); + loadOldBarPosition(experience, counts, barPositions.experienceBarPosition); + + CompletableFuture.supplyAsync(FancyStatusBars::loadBarConfig).thenAccept(object -> { + if (object != null) { + for (String s : object.keySet()) { + if (statusBars.containsKey(s)) { + try { + statusBars.get(s).loadFromJson(object.get(s).getAsJsonObject()); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load {} status bar", s, e); + } + } else { + LOGGER.warn("[Skyblocker] Unknown status bar: {}", s); + } + } + } + placeBarsInPositioner(); + configLoaded = true; + }).exceptionally(throwable -> { + LOGGER.error("[Skyblocker] Failed reading status bars config", throwable); + return null; + }); + ClientLifecycleEvents.CLIENT_STOPPING.register((client) -> { + saveBarConfig(); + GLFW.glfwDestroyCursor(StatusBarsConfigScreen.RESIZE_CURSOR); + }); + + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register( + ClientCommandManager.literal(SkyblockerMod.NAMESPACE) + .then(ClientCommandManager.literal("bars").executes(Scheduler.queueOpenScreenCommand(StatusBarsConfigScreen::new))))); + } + + /** + * Loads the bar position from the old config + * @param bar the bar to load the position for + * @param counts the counts for each bar position (LAYER1, LAYER2, RIGHT) + * @param position the position to load + */ + private static void loadOldBarPosition(StatusBar bar, int[] counts, SkyblockerConfig.OldBarPosition position) { + switch (position) { + case RIGHT: + bar.anchor = BarPositioner.BarAnchor.HOTBAR_RIGHT; + bar.gridY = 0; + bar.gridX = counts[position.ordinal()]++; + break; + case LAYER1: + bar.anchor = BarPositioner.BarAnchor.HOTBAR_TOP; + bar.gridY = 0; + bar.gridX = counts[position.ordinal()]++; + break; + case LAYER2: + bar.anchor = BarPositioner.BarAnchor.HOTBAR_TOP; + bar.gridY = 1; + bar.gridX = counts[position.ordinal()]++; + break; + } + } + + private static boolean configLoaded = false; + + private static void placeBarsInPositioner() { + List<StatusBar> original = statusBars.values().stream().toList(); + + for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { + List<StatusBar> barList = new ArrayList<>(original.stream().filter(bar -> bar.anchor == barAnchor).toList()); + if (barList.isEmpty()) continue; + barList.sort((a, b) -> a.gridY == b.gridY ? Integer.compare(a.gridX, b.gridX) : Integer.compare(a.gridY, b.gridY)); + + int y = -1; + int rowNum = -1; + for (StatusBar statusBar : barList) { + if (statusBar.gridY > y) { + barPositioner.addRow(barAnchor); + rowNum++; + y = statusBar.gridY; + } + barPositioner.addBar(barAnchor, rowNum, statusBar); + } + } + } + + public static JsonObject loadBarConfig() { + try (BufferedReader reader = Files.newBufferedReader(FILE)) { + return SkyblockerMod.GSON.fromJson(reader, JsonObject.class); + } catch (NoSuchFileException e) { + LOGGER.warn("[Skyblocker] No status bar config file found, using defaults"); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to load status bars config", e); + } + return null; + } + + public static void saveBarConfig() { + JsonObject output = new JsonObject(); + statusBars.forEach((s, statusBar) -> output.add(s, statusBar.toJson())); + try (BufferedWriter writer = Files.newBufferedWriter(FILE)) { + SkyblockerMod.GSON.toJson(output, writer); + LOGGER.info("[Skyblocker] Saved status bars config"); + } catch (IOException e) { + LOGGER.error("[Skyblocker] Failed to save status bars config", e); + } + } + + public static void updatePositions() { + if (!configLoaded) return; + final int width = MinecraftClient.getInstance().getWindow().getScaledWidth(); + final int height = MinecraftClient.getInstance().getWindow().getScaledHeight(); + + for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { + ScreenPos anchorPosition = barAnchor.getAnchorPosition(width, height); + BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); + + if (sizeRule.isTargetSize()) { + for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) { + LinkedList<StatusBar> barRow = barPositioner.getRow(barAnchor, row); + if (barRow.isEmpty()) continue; + + // FIX SIZES + int totalSize = 0; + for (StatusBar statusBar : barRow) + totalSize += (statusBar.size = MathHelper.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize())); + + whileLoop: + while (totalSize != sizeRule.targetSize()) { + if (totalSize > sizeRule.targetSize()) { + for (StatusBar statusBar : barRow) { + if (statusBar.size > sizeRule.minSize()) { + statusBar.size--; + totalSize--; + if (totalSize == sizeRule.targetSize()) break whileLoop; + } + } + } else { + for (StatusBar statusBar : barRow) { + if (statusBar.size < sizeRule.maxSize()) { + statusBar.size++; + totalSize++; + if (totalSize == sizeRule.targetSize()) break whileLoop; + } + } + } + } + + } + } + + for (int row = 0; row < barPositioner.getRowCount(barAnchor); row++) { + List<StatusBar> barRow = barPositioner.getRow(barAnchor, row); + if (barRow.isEmpty()) continue; + + + // Update the positions + float widthPerSize; + if (sizeRule.isTargetSize()) + widthPerSize = (float) sizeRule.totalWidth() / sizeRule.targetSize(); + else + widthPerSize = sizeRule.widthPerSize(); + + int currSize = 0; + int rowSize = barRow.size(); + for (int i = 0; i < rowSize; i++) { + // A bit of a padding + int offsetX = 0; + int lessWidth = 0; + if (rowSize > 1) { // Technically bars in the middle of 3+ bars will be smaller than the 2 side ones but shh + if (i == 0) lessWidth = 1; + else if (i == rowSize - 1) { + lessWidth = 1; + offsetX = 1; + } else { + lessWidth = 2; + offsetX = 1; + } + } + StatusBar statusBar = barRow.get(i); + statusBar.size = MathHelper.clamp(statusBar.size, sizeRule.minSize(), sizeRule.maxSize()); + + float x = barAnchor.isRight() ? + anchorPosition.x() + currSize * widthPerSize : + anchorPosition.x() - currSize * widthPerSize - statusBar.size * widthPerSize; + statusBar.setX(MathHelper.ceil(x) + offsetX); + + int y = barAnchor.isUp() ? + anchorPosition.y() - (row + 1) * (statusBar.getHeight() + 1) : + anchorPosition.y() + row * (statusBar.getHeight() + 1); + statusBar.setY(y); + + statusBar.setWidth(MathHelper.floor(statusBar.size * widthPerSize) - lessWidth); + currSize += statusBar.size; + statusBar.gridX = i; + statusBar.gridY = row; + + } + } + + } + } + + public boolean render(DrawContext context, int scaledWidth, int scaledHeight) { + var player = client.player; + if (!SkyblockerConfigManager.get().general.bars.enableBars || player == null || Utils.isInTheRift()) + return false; + + Collection<StatusBar> barCollection = statusBars.values(); + for (StatusBar value : barCollection) { + value.render(context, -1, -1, client.getLastFrameDuration()); + } + for (StatusBar statusBar : barCollection) { + if (statusBar.showText()) statusBar.renderText(context); + } + StatusBarTracker.Resource health = statusBarTracker.getHealth(); + statusBars.get("health").updateValues(health.value() / (float) health.max(), health.overflow() / (float) health.max(), health.value()); + + StatusBarTracker.Resource intelligence = statusBarTracker.getMana(); + statusBars.get("intelligence").updateValues(intelligence.value() / (float) intelligence.max(), intelligence.overflow() / (float) intelligence.max(), intelligence.value()); + int defense = statusBarTracker.getDefense(); + statusBars.get("defense").updateValues(defense / (defense + 100.f), 0, defense); + statusBars.get("experience").updateValues(player.experienceProgress, 0, player.experienceLevel); + return true; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java new file mode 100644 index 00000000..f8109852 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBar.java @@ -0,0 +1,353 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.*; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.Widget; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.function.Consumer; + +public class StatusBar implements Widget, Drawable, Element, Selectable { + + private static final Identifier BAR_FILL = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_fill"); + private static final Identifier BAR_BACK = new Identifier(SkyblockerMod.NAMESPACE, "bars/bar_back"); + + + /* public static final Codec<StatusBar> CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("size").forGetter(bar -> bar.size), + Codec.INT.fieldOf("x").forGetter(bar -> bar.gridX), + Codec.INT.fieldOf("y").forGetter(bar -> bar.gridY), + Codec.STRING.listOf().fieldOf("colors").xmap( + strings -> strings.stream().map(s -> Integer.parseInt(s, 16)).map(Color::new).toArray(Color[]::new), + colors -> Arrays.stream(colors).map(color -> Integer.toHexString(color.getRGB())).toList()) + .forGetter(StatusBar::getColors), + Codec.STRING.optionalFieldOf("text_color").xmap( + s -> { + if (s.isPresent()) { + return Optional.of(new Color(Integer.parseInt(s.get(), 16))); + } else return Optional.empty(); + }, + o -> o.map(object -> Integer.toHexString(((Color) object).getRGB()))) + .forGetter(bar -> { + if (bar.getTextColor() != null) { + return Optional.of(bar.getTextColor()); + } else return Optional.empty(); + }), + Codec.BOOL.optionalFieldOf("show_text", true).forGetter(StatusBar::showText), + Codec.STRING.fieldOf("icon_position").xmap( + IconPosition::valueOf, + Enum::toString + ).forGetter(bar -> bar.iconPosition) + ) + + .apply(instance, ));*/ + + private final Identifier icon; + + public Color[] getColors() { + return colors; + } + + public boolean hasOverflow() { + return hasOverflow; + } + + public @Nullable Color getTextColor() { + return textColor; + } + + private Color[] colors; + private final boolean hasOverflow; + + public void setTextColor(@Nullable Color textColor) { + this.textColor = textColor; + } + + private @Nullable Color textColor; + + public Text getName() { + return name; + } + + private final Text name; + + private @Nullable OnClick onClick = null; + public int gridX = 0; + public int gridY = 0; + public @Nullable BarPositioner.BarAnchor anchor = null; + + public int size = 1; + private int width = 0; + + public float fill = 0; + public float overflowFill = 0; + public boolean ghost = false; + + private Object value = ""; + + private int x = 0; + private int y = 0; + + private IconPosition iconPosition = IconPosition.LEFT; + private boolean showText = true; + + public StatusBar(Identifier icon, Color[] colors, boolean hasOverflow, @Nullable Color textColor, Text name) { + this.icon = icon; + this.colors = colors; + this.hasOverflow = hasOverflow; + this.textColor = textColor; + this.name = name; + } + + public StatusBar(Identifier icon, Color[] colors, boolean hasOverflow, @Nullable Color textColor) { + this(icon, colors, hasOverflow, textColor, Text.empty()); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + if (width <= 0) return; + // half works lol. only puts transparency on the filler of the bar + if (ghost) context.setShaderColor(1f, 1f, 1f, 0.25f); + switch (iconPosition) { + case LEFT -> context.drawGuiTexture(icon, x, y, 9, 9); + case RIGHT -> context.drawGuiTexture(icon, x + width - 9, y, 9, 9); + } + + int barWith = iconPosition.equals(IconPosition.OFF) ? width : width - 10; + int barX = iconPosition.equals(IconPosition.LEFT) ? x + 10 : x; + context.drawGuiTexture(BAR_BACK, barX, y + 1, barWith, 7); + RenderHelper.renderNineSliceColored(context, BAR_FILL, barX + 1, y + 2, (int) ((barWith - 2) * fill), 5, colors[0]); + + + if (hasOverflow && overflowFill > 0) { + RenderHelper.renderNineSliceColored(context, BAR_FILL, barX + 1, y + 2, (int) ((barWith - 2) * overflowFill), 5, colors[1]); + } + if (ghost) context.setShaderColor(1f, 1f, 1f, 1f); + //context.drawText(MinecraftClient.getInstance().textRenderer, gridX + " " + gridY + " s:" + size , x, y-9, Colors.WHITE, true); + } + + public void updateValues(float fill, float overflowFill, Object text) { + this.value = text; + this.fill = fill; + this.overflowFill = overflowFill; + } + + public void renderText(DrawContext context) { + TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; + int barWith = iconPosition.equals(IconPosition.OFF) ? width : width - 10; + int barX = iconPosition.equals(IconPosition.LEFT) ? x + 11 : x; + String text = this.value.toString(); + int x = barX + (barWith - textRenderer.getWidth(text)) / 2; + int y = this.y - 3; + + final int[] offsets = new int[]{-1, 1}; + for (int i : offsets) { + context.drawText(textRenderer, text, x + i, y, 0, false); + context.drawText(textRenderer, text, x, y + i, 0, false); + } + context.drawText(textRenderer, text, x, y, (textColor == null ? colors[0] : textColor).getRGB(), false); + } + + public void renderCursor(DrawContext context, int mouseX, int mouseY, float delta) { + int temp_x = x; + int temp_y = y; + int temp_width = width; + boolean temp_ghost = ghost; + + x = mouseX; + y = mouseY; + width = 100; + ghost = false; + + render(context, mouseX, mouseY, delta); + + x = temp_x; + y = temp_y; + width = temp_width; + ghost = temp_ghost; + } + + // GUI shenanigans + + @Override + public void setX(int x) { + this.x = x; + } + + @Override + public void setY(int y) { + this.y = y; + } + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + @Override + public int getHeight() { + return 9; + } + + @Override + public ScreenRect getNavigationFocus() { + return Widget.super.getNavigationFocus(); + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return mouseX >= x && mouseX <= x + getWidth() && mouseY >= y && mouseY <= y + getHeight(); + } + + @Override + public void forEachChild(Consumer<ClickableWidget> consumer) { + } + + @Override + public void setFocused(boolean focused) { + } + + @Override + public boolean isFocused() { + return false; + } + + @Override + public SelectionType getType() { + return SelectionType.NONE; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!isMouseOver(mouseX, mouseY)) return false; + if (onClick != null) { + onClick.onClick(this, button, (int) mouseX, (int) mouseY); + } + return true; + } + + public void setOnClick(@Nullable OnClick onClick) { + this.onClick = onClick; + } + + @Override + public void appendNarrations(NarrationMessageBuilder builder) { + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .append("name", name) + .append("gridX", gridX) + .append("gridY", gridY) + .append("size", size) + .append("x", x) + .append("y", y) + .append("width", width) + .append("anchor", anchor) + .toString(); + } + + public IconPosition getIconPosition() { + return iconPosition; + } + + public void setIconPosition(IconPosition iconPosition) { + this.iconPosition = iconPosition; + } + + public boolean showText() { + return showText; + } + + public void setShowText(boolean showText) { + this.showText = showText; + } + + public enum IconPosition { + LEFT, + RIGHT, + OFF + } + + @FunctionalInterface + public interface OnClick { + + void onClick(StatusBar statusBar, int button, int mouseX, int mouseY); + } + + public void loadFromJson(JsonObject object) { + // Make colors optional, so it's easy to reset to default + if (object.has("colors")) { + JsonArray colors1 = object.get("colors").getAsJsonArray(); + if (colors1.size() < 2 && hasOverflow) { + throw new IllegalStateException("Missing second color of bar that has overflow"); + } + Color[] newColors = new Color[colors1.size()]; + for (int i = 0; i < colors1.size(); i++) { + JsonElement jsonElement = colors1.get(i); + newColors[i] = new Color(Integer.parseInt(jsonElement.getAsString(), 16)); + } + this.colors = newColors; + } + + if (object.has("text_color")) this.textColor = new Color(Integer.parseInt(object.get("text_color").getAsString(), 16)); + + String maybeAnchor = object.get("anchor").getAsString().trim(); + this.anchor = maybeAnchor.equals("null") ? null : BarPositioner.BarAnchor.valueOf(maybeAnchor); + this.size = object.get("size").getAsInt(); + this.gridX = object.get("x").getAsInt(); + this.gridY = object.get("y").getAsInt(); + // these are optional too, why not + if (object.has("icon_position")) this.iconPosition = IconPosition.valueOf(object.get("icon_position").getAsString().trim()); + if (object.has("show_text")) this.showText = object.get("show_text").getAsBoolean(); + + } + + public JsonObject toJson() { + JsonObject object = new JsonObject(); + JsonArray colors1 = new JsonArray(); + for (Color color : colors) { + colors1.add(Integer.toHexString(color.getRGB()).substring(2)); + } + object.add("colors", colors1); + if (textColor != null) { + object.addProperty("text_color", Integer.toHexString(textColor.getRGB()).substring(2)); + } + object.addProperty("size", size); + if (anchor != null) { + object.addProperty("anchor", anchor.toString()); + } else object.addProperty("anchor", "null"); + object.addProperty("x", gridX); + object.addProperty("y", gridY); + object.addProperty("icon_position", iconPosition.toString()); + object.addProperty("show_text", showText); + return object; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java new file mode 100644 index 00000000..d1009a25 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/fancybars/StatusBarsConfigScreen.java @@ -0,0 +1,365 @@ +package de.hysky.skyblocker.skyblock.fancybars; + +import it.unimi.dsi.fastutil.Pair; +import it.unimi.dsi.fastutil.objects.ObjectBooleanMutablePair; +import it.unimi.dsi.fastutil.objects.ObjectObjectMutablePair; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.ScreenPos; +import net.minecraft.client.gui.ScreenRect; +import net.minecraft.client.gui.navigation.NavigationAxis; +import net.minecraft.client.gui.navigation.NavigationDirection; +import net.minecraft.client.gui.screen.PopupScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.util.Window; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; +import de.hysky.skyblocker.skyblock.fancybars.BarPositioner.BarLocation; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class StatusBarsConfigScreen extends Screen { + private static final Identifier HOTBAR_TEXTURE = new Identifier("hud/hotbar"); + + public static final long RESIZE_CURSOR = GLFW.glfwCreateStandardCursor(GLFW.GLFW_HRESIZE_CURSOR); + + private final Map<ScreenRect, BarLocation> rectToBarLocation = new HashMap<>(); + private static final int HOTBAR_WIDTH = 182; + + private @Nullable StatusBar cursorBar = null; + + public StatusBarsConfigScreen() { + super(Text.of("Status Bars Config")); + FancyStatusBars.updatePositions(); + } + + private BarLocation currentInsertLocation = new BarLocation(null, 0, 0); + + private final Pair<BarLocation, Boolean> resizeHover = new ObjectBooleanMutablePair<>(BarLocation.NULL, false); + + private final Pair<BarLocation, BarLocation> resizedBars = ObjectObjectMutablePair.of(BarLocation.NULL, BarLocation.NULL); + private boolean resizing = false; + + private EditBarWidget editBarWidget; + + // prioritize left and right cuz they are much smaller space than up and down + private static final NavigationDirection[] DIRECTION_CHECK_ORDER = new NavigationDirection[]{NavigationDirection.LEFT, NavigationDirection.RIGHT, NavigationDirection.UP, NavigationDirection.DOWN}; + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + /*for (ScreenRect screenRect : meaningFullName.keySet()) { + context.fillGradient(screenRect.position().x(), screenRect.position().y(), screenRect.position().x() + screenRect.width(), screenRect.position().y() + screenRect.height(), 0xFFFF0000, 0xFF0000FF); + }*/ + super.render(context, mouseX, mouseY, delta); + context.drawGuiTexture(HOTBAR_TEXTURE, width / 2 - HOTBAR_WIDTH / 2, height - 22, HOTBAR_WIDTH, 22); + editBarWidget.render(context, mouseX, mouseY, delta); + + ScreenRect mouseRect = new ScreenRect(new ScreenPos(mouseX - 1, mouseY - 1), 3, 3); + assert client != null; + Window window = client.getWindow(); + + if (cursorBar != null) { + cursorBar.renderCursor(context, mouseX, mouseY, delta); + boolean inserted = false; + rectLoop: + for (ScreenRect screenRect : rectToBarLocation.keySet()) { + for (NavigationDirection direction : DIRECTION_CHECK_ORDER) { + boolean overlaps = screenRect.getBorder(direction).add(direction).overlaps(mouseRect); + + if (overlaps) { + BarLocation barSnap = rectToBarLocation.get(screenRect); + if (barSnap.barAnchor() == null) break; + if (direction.getAxis().equals(NavigationAxis.VERTICAL)) { + int neighborInsertY = getNeighborInsertY(barSnap, !direction.isPositive()); + if (!currentInsertLocation.equals(barSnap.barAnchor(), barSnap.x(), neighborInsertY)) { + if (cursorBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar); + FancyStatusBars.barPositioner.addRow(barSnap.barAnchor(), neighborInsertY); + FancyStatusBars.barPositioner.addBar(barSnap.barAnchor(), neighborInsertY, cursorBar); + currentInsertLocation = BarLocation.of(cursorBar); + inserted = true; + } + } else { + int neighborInsertX = getNeighborInsertX(barSnap, direction.isPositive()); + if (!currentInsertLocation.equals(barSnap.barAnchor(), neighborInsertX, barSnap.y())) { + if (cursorBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar); + FancyStatusBars.barPositioner.addBar(barSnap.barAnchor(), barSnap.y(), neighborInsertX, cursorBar); + currentInsertLocation = BarLocation.of(cursorBar); + inserted = true; + } + } + break rectLoop; + } + } + } + if (inserted) { + FancyStatusBars.updatePositions(); + return; + } + // check for hovering empty anchors + for (BarPositioner.BarAnchor barAnchor : BarPositioner.BarAnchor.allAnchors()) { + if (FancyStatusBars.barPositioner.getRowCount(barAnchor) != 0) continue; + ScreenRect anchorHitbox = barAnchor.getAnchorHitbox(barAnchor.getAnchorPosition(width, height)); + context.fill(anchorHitbox.getLeft(), anchorHitbox.getTop(), anchorHitbox.getRight(), anchorHitbox.getBottom(), 0x99FFFFFF); + if (anchorHitbox.overlaps(mouseRect) && currentInsertLocation.barAnchor() != barAnchor) { + if (cursorBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(cursorBar.anchor, cursorBar.gridY, cursorBar); + FancyStatusBars.barPositioner.addRow(barAnchor); + FancyStatusBars.barPositioner.addBar(barAnchor, 0, cursorBar); + currentInsertLocation = BarLocation.of(cursorBar); + FancyStatusBars.updatePositions(); + } + } + } else { + if (resizing) { // actively resizing one or 2 bars + int middleX; + + BarLocation left = resizedBars.left(); + BarLocation right = resizedBars.right(); + boolean hasRight = right.barAnchor() != null; + boolean hasLeft = left.barAnchor() != null; + BarPositioner.BarAnchor barAnchor; + if (!hasRight) { + barAnchor = left.barAnchor(); + StatusBar bar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x()); + middleX = bar.getX() + bar.getWidth(); + } else { + barAnchor = right.barAnchor(); + middleX = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x()).getX(); + } + + boolean doResize = true; + StatusBar rightBar = null; + StatusBar leftBar = null; + + BarPositioner.SizeRule sizeRule = barAnchor.getSizeRule(); + + float widthPerSize; + if (sizeRule.isTargetSize()) + widthPerSize = (float) sizeRule.totalWidth() / sizeRule.targetSize(); + else + widthPerSize = sizeRule.widthPerSize(); + + // resize towards the left + if (mouseX < middleX) { + if (middleX - mouseX > widthPerSize / .75f) { + if (hasRight) { + rightBar = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x()); + if (rightBar.size + 1 > sizeRule.maxSize()) doResize = false; + } + if (hasLeft) { + leftBar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x()); + if (leftBar.size - 1 < sizeRule.minSize()) doResize = false; + } + + if (doResize) { + if (hasRight) rightBar.size++; + if (hasLeft) leftBar.size--; + FancyStatusBars.updatePositions(); + } + } + } else { // towards the right + if (mouseX - middleX > widthPerSize / .75f) { + if (hasRight) { + rightBar = FancyStatusBars.barPositioner.getBar(barAnchor, right.y(), right.x()); + if (rightBar.size - 1 < sizeRule.minSize()) doResize = false; + } + if (hasLeft) { + leftBar = FancyStatusBars.barPositioner.getBar(barAnchor, left.y(), left.x()); + if (leftBar.size + 1 > sizeRule.maxSize()) doResize = false; + } + + if (doResize) { + if (hasRight) rightBar.size--; + if (hasLeft) leftBar.size++; + FancyStatusBars.updatePositions(); + } + } + } + + } else { // hovering bars + rectLoop: + for (ScreenRect screenRect : rectToBarLocation.keySet()) { + for (NavigationDirection direction : new NavigationDirection[]{NavigationDirection.LEFT, NavigationDirection.RIGHT}) { + boolean overlaps = screenRect.getBorder(direction).add(direction).overlaps(mouseRect); + + if (overlaps && !editBarWidget.isMouseOver(mouseX, mouseY)) { + BarLocation barLocation = rectToBarLocation.get(screenRect); + if (barLocation.barAnchor() == null) break; + boolean right = direction.equals(NavigationDirection.RIGHT); + // can't resize on the edge of a target size row! + if (barLocation.barAnchor().getSizeRule().isTargetSize() && !FancyStatusBars.barPositioner.hasNeighbor(barLocation.barAnchor(), barLocation.y(), barLocation.x(), right)) { + break; + } + if (!barLocation.barAnchor().getSizeRule().isTargetSize() && barLocation.x() == 0 && barLocation.barAnchor().isRight() != right) + break; + resizeHover.first(barLocation); + resizeHover.right(right); + GLFW.glfwSetCursor(window.getHandle(), RESIZE_CURSOR); + break rectLoop; + } else { + resizeHover.first(BarLocation.NULL); + GLFW.glfwSetCursor(window.getHandle(), 0); + } + } + } + } + } + } + + private static int getNeighborInsertX(BarLocation barLocation, boolean right) { + BarPositioner.BarAnchor barAnchor = barLocation.barAnchor(); + int gridX = barLocation.x(); + if (barAnchor == null) return 0; + if (right) { + return barAnchor.isRight() ? gridX + 1 : gridX; + } else { + return barAnchor.isRight() ? gridX : gridX + 1; + } + } + + private static int getNeighborInsertY(BarLocation barLocation, boolean up) { + BarPositioner.BarAnchor barAnchor = barLocation.barAnchor(); + int gridY = barLocation.y(); + if (barAnchor == null) return 0; + if (up) { + return barAnchor.isUp() ? gridY + 1 : gridY; + } else { + return barAnchor.isUp() ? gridY : gridY + 1; + } + } + + @Override + protected void init() { + super.init(); + editBarWidget = new EditBarWidget(0, 0, this); + editBarWidget.visible = false; + addSelectableChild(editBarWidget); // rendering separately to have it above hotbar + Collection<StatusBar> values = FancyStatusBars.statusBars.values(); + values.forEach(this::setup); + checkNullAnchor(values); + updateScreenRects(); + this.addDrawableChild(ButtonWidget.builder(Text.literal("?"), + button -> { + assert client != null; + client.setScreen(new PopupScreen.Builder(this, Text.translatable("skyblocker.bars.config.explanationTitle")) + .button(Text.translatable("gui.ok"), PopupScreen::close) + .message(Text.translatable("skyblocker.bars.config.explanation")) + .build()); + }) + .dimensions(width - 20, (height - 15) / 2, 15, 15) + .build()); + } + + private void setup(StatusBar statusBar) { + this.addDrawableChild(statusBar); + statusBar.setOnClick(this::onBarClick); + } + + private static void checkNullAnchor(Iterable<StatusBar> bars) { + int offset = 0; + for (StatusBar statusBar : bars) { + if (statusBar.anchor == null) { + statusBar.setX(5); + statusBar.setY(50 + offset); + statusBar.setWidth(30); + offset += statusBar.getHeight(); + } + } + } + + @Override + public void removed() { + super.removed(); + FancyStatusBars.statusBars.values().forEach(statusBar -> statusBar.setOnClick(null)); + if (cursorBar != null) cursorBar.ghost = false; + FancyStatusBars.updatePositions(); + assert client != null; + GLFW.glfwSetCursor(client.getWindow().getHandle(), 0); + FancyStatusBars.saveBarConfig(); + } + + @Override + public boolean shouldPause() { + return false; + } + + private void onBarClick(StatusBar statusBar, int button, int mouseX, int mouseY) { + if (button == 0) { + cursorBar = statusBar; + cursorBar.ghost = true; + if (statusBar.anchor != null) + FancyStatusBars.barPositioner.removeBar(statusBar.anchor, statusBar.gridY, statusBar); + FancyStatusBars.updatePositions(); + cursorBar.setX(width + 5); // send it to limbo lol + updateScreenRects(); + } else if (button == 1) { + int x = Math.min(mouseX - 1, width - editBarWidget.getWidth()); + int y = Math.min(mouseY - 1, height - editBarWidget.getHeight()); + editBarWidget.visible = true; + editBarWidget.setStatusBar(statusBar); + editBarWidget.setX(x); + editBarWidget.setY(y); + } + } + + private void updateScreenRects() { + rectToBarLocation.clear(); + FancyStatusBars.statusBars.values().forEach(statusBar1 -> { + if (statusBar1.anchor == null) return; + rectToBarLocation.put( + new ScreenRect(new ScreenPos(statusBar1.getX(), statusBar1.getY()), statusBar1.getWidth(), statusBar1.getHeight()), + BarLocation.of(statusBar1)); + }); + } + + @Override + public boolean mouseReleased(double mouseX, double mouseY, int button) { + if (cursorBar != null) { + cursorBar.ghost = false; + cursorBar = null; + FancyStatusBars.updatePositions(); + checkNullAnchor(FancyStatusBars.statusBars.values()); + updateScreenRects(); + return true; + } else if (resizing) { + resizing = false; + resizedBars.left(BarLocation.NULL); + resizedBars.right(BarLocation.NULL); + updateScreenRects(); + return true; + } + return super.mouseReleased(mouseX, mouseY, button); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + BarLocation first = resizeHover.first(); + // want the right click thing to have priority + if (!editBarWidget.isMouseOver(mouseX, mouseY) && button == 0 && !first.equals(BarLocation.NULL)) { + BarPositioner.BarAnchor barAnchor = first.barAnchor(); + assert barAnchor != null; + if (resizeHover.right()) { + resizedBars.left(first); + + if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.y(), first.x(), true)) { + resizedBars.right(new BarLocation(barAnchor, first.x() + (barAnchor.isRight() ? 1 : -1), first.y())); + } else resizedBars.right(BarLocation.NULL); + } else { + resizedBars.right(first); + + if (FancyStatusBars.barPositioner.hasNeighbor(barAnchor, first.y(), first.x(), false)) { + resizedBars.left(new BarLocation(barAnchor, first.x() + (barAnchor.isRight() ? -1 : 1), first.y())); + } else resizedBars.left(BarLocation.NULL); + } + resizing = true; + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } +} |