package net.fretux.skillengine.client; import com.mojang.math.Axis; import net.fretux.ascend.player.PlayerStatsProvider; import net.fretux.skillengine.network.PacketHandler; import net.fretux.skillengine.network.ServerboundUnlockNodePacket; import net.fretux.skillengine.skilltree.AbilityNode; import net.fretux.skillengine.skilltree.AbilityNodeRegistry; import net.fretux.skillengine.skilltree.SkillNode; import net.fretux.skillengine.skilltree.SkillNodeRegistry; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.FormattedText; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.Mth; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; public class SkilltreeScreen extends Screen { private static final int NODE_RADIUS = 12; private static final int ABILITY_RADIUS = 12; private static final int NODE_ICON_SIZE = 16; private static final int OVERLAY_WIDTH = 240; private static final int OVERLAY_HEIGHT = 180; private static final int OVERLAY_CONTENT_HEIGHT = 160; private static final float ZOOM_STEP = 0.1f; private static final float LINE_THICKNESS = 2.0f; private static final int GRID_SIZE = 32; private static final int PANEL_BORDER = 0xFF53647A; private static final int PANEL_BACKGROUND = 0xF0181D26; private float zoom = 1.0f; private double panX = 0; private double panY = 0; private boolean dragging = false; private SkillNode hoveredNode = null; private SkillNode selectedNode = null; private AbilityNode hoveredAbility = null; private AbilityNode selectedAbility = null; private ResourceLocation activeTree = null; private ResourceLocation highlightedNode = null; private ResourceLocation highlightedAbility = null; private boolean highlightSkillPoints = false; private Component unlockFailureMessage = null; public SkilltreeScreen() { super(Component.literal("Ascend Skilltree")); } @Override protected void init() { super.init(); if (activeTree == null || !SkillNodeRegistry.trees().contains(activeTree)) { List trees = SkillNodeRegistry.trees(); activeTree = trees.isEmpty() ? null : trees.get(0); } rebuildTreeTabs(); } @Override public boolean mouseScrolled(double mouseX, double mouseY, double delta) { zoom += (float) (delta * ZOOM_STEP); float minZoom = 0.5f; float maxZoom = 2.0f; zoom = Mth.clamp(zoom, minZoom, maxZoom); return true; } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (selectedNode != null || selectedAbility != null) { return super.mouseClicked(mouseX, mouseY, button); } if (button == 0) { SkillNode clickedNode = findNodeAt(mouseX, mouseY); if (clickedNode != null) { if (isLockedByExclusivity(clickedNode)) { assert Minecraft.getInstance().player != null; Minecraft.getInstance().player.playSound( net.minecraft.sounds.SoundEvents.VILLAGER_NO, 1f, 1f ); return true; } if (SkilltreeClientState.isUnlocked(clickedNode.getId())) { return true; } selectedNode = clickedNode; rebuildOverlayButtons(); return true; } AbilityNode clickedAbility = findAbilityAt(mouseX, mouseY); if (clickedAbility != null) { if (SkilltreeClientState.isAbilityUnlocked(clickedAbility.getId())) { Minecraft.getInstance().setScreen(new AbilityBindingScreen(clickedAbility)); } else { selectedAbility = clickedAbility; rebuildAbilityOverlayButtons(); } return true; } dragging = true; } return super.mouseClicked(mouseX, mouseY, button); } @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) { if (selectedNode != null || selectedAbility != null) { return super.mouseDragged(mouseX, mouseY, button, dx, dy); } if (dragging && button == 0) { panX += dx; panY += dy; return true; } return super.mouseDragged(mouseX, mouseY, button, dx, dy); } @Override public boolean mouseReleased(double mouseX, double mouseY, int button) { if (button == 0) { dragging = false; } return super.mouseReleased(mouseX, mouseY, button); } @Override public void render(@NotNull GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) { this.renderBackground(gfx); renderTreeBackdrop(gfx); renderSkillPointHeader(gfx); hoveredNode = findNodeAt(mouseX, mouseY); hoveredAbility = findAbilityAt(mouseX, mouseY); drawGraph(gfx); renderUnlockFailure(gfx); if (hoveredNode != null && selectedNode == null && selectedAbility == null) { renderNodeTooltip(gfx, hoveredNode, mouseX, mouseY); } if (selectedNode != null) { renderNodeOverlay(gfx, selectedNode); } else if (selectedAbility != null) { renderAbilityOverlay(gfx, selectedAbility); } super.render(gfx, mouseX, mouseY, partialTicks); } private void renderSkillPointHeader(GuiGraphics gfx) { int remaining = SkilltreeClientState.getCurrentSkillPoints(); Component pointsText = Component.literal("Skill Points ") .withStyle(ChatFormatting.GRAY) .append(Component.literal(String.valueOf(remaining)).withStyle(ChatFormatting.GOLD)); int panelWidth = font.width(pointsText) + 16; gfx.fill(7, 6, 7 + panelWidth, 24, 0xAA080B10); gfx.fill(7, 6, 9, 24, highlightSkillPoints ? 0xFFFF4545 : 0xFFFFC857); if (highlightSkillPoints) { gfx.fill(5, 4, 7 + panelWidth + 2, 6, 0xFFFF4545); gfx.fill(5, 24, 7 + panelWidth + 2, 26, 0xFFFF4545); gfx.fill(5, 4, 7, 26, 0xFFFF4545); gfx.fill(7 + panelWidth, 4, 7 + panelWidth + 2, 26, 0xFFFF4545); } gfx.drawString(font, pointsText, 14, 11, 0xFFFFFF, true); Component hint = Component.literal("Drag to pan • Scroll to zoom") .withStyle(ChatFormatting.DARK_GRAY); gfx.drawString(font, hint, width - font.width(hint) - 10, 11, 0xFFFFFF, false); } private void renderTreeBackdrop(GuiGraphics gfx) { gfx.fill(0, 0, width, height, 0xD00B0E14); int spacing = Math.max(16, (int) (GRID_SIZE * zoom)); int offsetX = Math.floorMod((int) panX + width / 2, spacing); int offsetY = Math.floorMod((int) panY + height / 2, spacing); for (int x = offsetX; x < width; x += spacing) { gfx.fill(x, 0, x + 1, height, 0x182A3442); } for (int y = offsetY; y < height; y += spacing) { gfx.fill(0, y, width, y + 1, 0x182A3442); } gfx.fill(0, 0, width, 1, 0x8053647A); } private void drawGraph(GuiGraphics gfx) { for (SkillNode node : activeSkillNodes()) { int[] p1 = worldToScreen(node.getX(), node.getY()); for (ResourceLocation neighborId : node.getLinks()) { SkillNode neighbor = SkillNodeRegistry.get(neighborId); if (neighbor == null) continue; int[] p2 = worldToScreen(neighbor.getX(), neighbor.getY()); boolean bothUnlocked = SkilltreeClientState.isUnlocked(node.getId()) && SkilltreeClientState.isUnlocked(neighbor.getId()); int linkColor = bothUnlocked ? 0xFF6EDC8A : 0xFF4A5361; drawConnection(gfx, p1[0], p1[1], p2[0], p2[1], linkColor); } } for (AbilityNode ability : AbilityNodeRegistry.all()) { int[] p1 = worldToScreen(ability.getX(), ability.getY()); for (ResourceLocation link : ability.getLinks()) { SkillNode node = SkillNodeRegistry.get(link); AbilityNode ability2 = AbilityNodeRegistry.get(link); if (node != null) { int[] p2 = worldToScreen(node.getX(), node.getY()); drawConnection(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6579C7); } else if (ability2 != null) { int[] p2 = worldToScreen(ability2.getX(), ability2.getY()); drawConnection(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6579C7); } } } for (AbilityNode ability : AbilityNodeRegistry.all()) { int[] pos = worldToScreen(ability.getX(), ability.getY()); int radius = getRenderedNodeRadius(ABILITY_RADIUS); boolean unlocked = SkilltreeClientState.isAbilityUnlocked(ability.getId()); int color; if (ability.getId().equals(highlightedAbility)) { color = 0xFFFF4545; } else if (ability == selectedAbility) { color = 0xFFAA8833; } else if (ability == hoveredAbility) { color = 0xFF8888AA; } else if (unlocked) { color = 0xFF3399FF; } else { color = 0xFF223355; } renderNodeFrame(gfx, pos[0], pos[1], radius, color, ability == hoveredAbility, ability == selectedAbility, true); ResourceLocation icon = ability.getIcon(); if (icon != null) { int size = getRenderedNodeIconSize(); gfx.blit(icon, pos[0] - size / 2, pos[1] - size / 2, size, size, 0, 0, NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE); } } for (SkillNode node : activeSkillNodes()) { int[] pos = worldToScreen(node.getX(), node.getY()); int radius = getRenderedNodeRadius(NODE_RADIUS); int color = getNodeColor(node); renderNodeFrame(gfx, pos[0], pos[1], radius, color, node == hoveredNode, node == selectedNode, false); ResourceLocation icon = node.getIcons(); if (icon != null) { int size = getRenderedNodeIconSize(); gfx.blit( icon, pos[0] - size / 2, pos[1] - size / 2, size, size, 0, 0, NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE ); } } } private void renderNodeFrame(GuiGraphics gfx, int cx, int cy, int radius, int color, boolean hovered, boolean selected, boolean ability) { int glow = ability ? 0x554B79D8 : 0x554FCB72; if (hovered || selected) { int spread = selected ? 5 : 3; gfx.fill(cx - radius - spread, cy - radius - spread, cx + radius + spread, cy + radius + spread, glow); } gfx.fill(cx - radius - 2, cy - radius, cx + radius + 2, cy + radius, 0xAA05070A); gfx.fill(cx - radius, cy - radius - 2, cx + radius, cy + radius + 2, 0xAA05070A); int border = selected ? 0xFFFFD36A : hovered ? 0xFFE4EBF5 : ability ? 0xFF829BEB : 0xFF778394; gfx.fill(cx - radius - 1, cy - radius - 1, cx + radius + 1, cy + radius + 1, border); gfx.fill(cx - radius + 1, cy - radius + 1, cx + radius - 1, cy + radius - 1, color); gfx.fill(cx - radius + 2, cy - radius + 2, cx + radius - 2, cy - radius + 4, 0x35FFFFFF); } private void renderNodeTooltip(GuiGraphics gfx, SkillNode node, int mouseX, int mouseY) { List tooltip = new ArrayList<>(); tooltip.add(node.getTitle()); String desc = node.getDescription().getString(); List wrapped = font.getSplitter().splitLines( desc, 220, node.getDescription().getStyle() ); for (FormattedText ft : wrapped) { tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY)); } tooltip.add(Component.literal("Cost: " + node.getCost()) .withStyle(ChatFormatting.DARK_AQUA)); if (!node.getPrereqAttributes().isEmpty()) { tooltip.add(Component.literal("Requirements:") .withStyle(ChatFormatting.GOLD)); assert Minecraft.getInstance().player != null; Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS) .ifPresent(stats -> node.getPrereqAttributes().forEach((attr, required) -> { int current = stats.getAttributeLevel(attr); boolean ok = current >= required; tooltip.add( Component.literal(" - " + attr + ": " + current + "/" + required) .withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED) ); })); } if (!hoveredNode.getExclusiveWith().isEmpty()) { tooltip.add(Component.literal("Mutually Exclusive with:") .withStyle(ChatFormatting.RED)); for (ResourceLocation ex : hoveredNode.getExclusiveWith()) { SkillNode other = SkillNodeRegistry.get(ex); Component name = other != null ? other.getTitle() : Component.literal(ex.toString()); tooltip.add(Component.literal(" - ").append(name.copy()) .withStyle(ChatFormatting.RED)); } } gfx.renderComponentTooltip(font, tooltip, mouseX, mouseY); } private void renderNodeOverlay(GuiGraphics gfx, SkillNode node) { if (isLockedByExclusivity(node)) { return; } int x = (width - OVERLAY_WIDTH) / 2; int y = (height - OVERLAY_HEIGHT) / 2; gfx.fill(0, 0, width, height, 0x88000000); renderOverlayPanel(gfx, x, y); gfx.drawCenteredString(font, node.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF); gfx.drawWordWrap(font, node.getDescription(), x + 10, y + 30, OVERLAY_WIDTH - 20, 0xDDDDDD); final int[] textY = {y + 95}; gfx.drawString( font, Component.literal("Cost: " + node.getCost() + " skill points") .withStyle(ChatFormatting.AQUA), x + 10, textY[0], 0xAAAAAA ); textY[0] += 15; if (!node.getPrereqAttributes().isEmpty()) { gfx.drawString( font, Component.literal("Requirements:") .withStyle(ChatFormatting.GOLD), x + 10, textY[0], 0xFFFFFF ); textY[0] += 12; assert Minecraft.getInstance().player != null; Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS) .ifPresent(stats -> node.getPrereqAttributes().forEach((attr, required) -> { int current = stats.getAttributeLevel(attr); boolean ok = current >= required; Component line = Component.literal( " - " + attr + ": " + current + "/" + required ).withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED); gfx.drawString(font, line, x + 10, textY[0], 0xFFFFFF); textY[0] += 12; })); } } private void renderAbilityOverlay(GuiGraphics gfx, AbilityNode ability) { int x = (width - OVERLAY_WIDTH) / 2; int y = (height - OVERLAY_HEIGHT) / 2; gfx.fill(0, 0, width, height, 0x88000000); renderOverlayPanel(gfx, x, y); gfx.drawCenteredString(font, ability.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF); int textY = y + 30; if (ability.getDescription() != null) { gfx.drawWordWrap(font, ability.getDescription(), x + 10, textY, OVERLAY_WIDTH - 20, 0xDDDDDD); textY += 60; } gfx.drawString(font, Component.literal("Cooldown: " + ability.getCooldown() / 20 + " seconds") .withStyle(ChatFormatting.AQUA), x + 10, textY, 0xFFFFFF); textY += 15; if (!ability.getLinks().isEmpty()) { gfx.drawString(font, Component.literal("Requires:") .withStyle(ChatFormatting.GOLD), x + 10, textY, 0xFFFFFF); textY += 12; for (ResourceLocation parent : ability.getLinks()) { SkillNode parentNode = SkillNodeRegistry.get(parent); AbilityNode parentAbility = AbilityNodeRegistry.get(parent); Component name = parentNode != null ? parentNode.getTitle() : parentAbility != null ? parentAbility.getTitle() : Component.literal(parent.toString()); gfx.drawString(font, Component.literal(" - ").append(name), x + 10, textY, 0xFFFFFF); textY += 12; } } } private void renderOverlayPanel(GuiGraphics gfx, int x, int y) { gfx.fill(x - 2, y - 2, x + OVERLAY_WIDTH + 2, y + OVERLAY_HEIGHT + 2, 0x99000000); gfx.fill(x - 1, y - 1, x + OVERLAY_WIDTH + 1, y + OVERLAY_HEIGHT + 1, PANEL_BORDER); gfx.fill(x, y, x + OVERLAY_WIDTH, y + OVERLAY_HEIGHT, PANEL_BACKGROUND); gfx.fill(x, y, x + OVERLAY_WIDTH, y + 3, 0xFF6C83A3); gfx.fill(x + 8, y + 23, x + OVERLAY_WIDTH - 8, y + 24, 0x6653647A); } private void rebuildOverlayButtons() { clearWidgets(); if (selectedNode == null) { return; } int x = (width - OVERLAY_WIDTH) / 2; int y = (height - OVERLAY_CONTENT_HEIGHT) / 2; boolean alreadyUnlocked = SkilltreeClientState.isUnlocked(selectedNode.getId()); if (!alreadyUnlocked) { addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> { if (!prepareSkillUnlock(selectedNode)) { selectedNode = null; clearWidgets(); rebuildTreeTabs(); return; } PacketHandler.CHANNEL.sendToServer( new ServerboundUnlockNodePacket(selectedNode.getId()) ); selectedNode = null; clearWidgets(); rebuildTreeTabs(); }).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20) .size(80, 20) .build()); } addRenderableWidget(Button.builder(Component.literal("Close"), btn -> { selectedNode = null; clearWidgets(); rebuildTreeTabs(); }).pos(x + OVERLAY_WIDTH - 60, y - 5) .size(50, 20) .build()); } private void rebuildAbilityOverlayButtons() { clearWidgets(); if (selectedAbility == null) { return; } int x = (width - OVERLAY_WIDTH) / 2; int y = (height - OVERLAY_CONTENT_HEIGHT) / 2; boolean unlocked = SkilltreeClientState.isAbilityUnlocked(selectedAbility.getId()); if (!unlocked) { addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> { if (!prepareAbilityUnlock(selectedAbility)) { selectedAbility = null; clearWidgets(); rebuildTreeTabs(); return; } PacketHandler.CHANNEL.sendToServer( new ServerboundUnlockNodePacket(selectedAbility.getId()) ); selectedAbility = null; clearWidgets(); rebuildTreeTabs(); }).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20) .size(80, 20) .build()); } addRenderableWidget(Button.builder(Component.literal("Close"), btn -> { selectedAbility = null; clearWidgets(); rebuildTreeTabs(); }).pos(x + OVERLAY_WIDTH - 60, y - 5) .size(50, 20) .build()); } private int[] worldToScreen(float wx, float wy) { double sx = width / 2.0 + panX + wx * zoom; double sy = height / 2.0 + panY - wy * zoom; return new int[]{(int) sx, (int) sy}; } private SkillNode findNodeAt(double mouseX, double mouseY) { int radius = getRenderedNodeRadius(NODE_RADIUS); for (SkillNode node : activeSkillNodes()) { int[] pos = worldToScreen(node.getX(), node.getY()); if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], radius)) { return node; } } return null; } private AbilityNode findAbilityAt(double mouseX, double mouseY) { int radius = getRenderedNodeRadius(ABILITY_RADIUS); for (AbilityNode ability : AbilityNodeRegistry.all()) { int[] pos = worldToScreen(ability.getX(), ability.getY()); if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], radius)) { return ability; } } return null; } private int getRenderedNodeRadius(int baseRadius) { return Math.max(1, Math.round(baseRadius * Math.min(1.0f, zoom))); } private int getRenderedNodeIconSize() { return Math.max(1, Math.round(NODE_ICON_SIZE * Math.min(1.0f, zoom))); } private boolean isPointInCircle(double mx, double my, int cx, int cy, int radius) { return mx >= cx - radius && mx <= cx + radius && my >= cy - radius && my <= cy + radius; } private boolean isLockedByExclusivity(SkillNode node) { return node.getExclusiveWith().stream().anyMatch(SkilltreeClientState::isUnlocked); } private int getNodeColor(SkillNode node) { boolean unlocked = SkilltreeClientState.isUnlocked(node.getId()); boolean lockedByExclusivity = isLockedByExclusivity(node); int color; if (node.getId().equals(highlightedNode)) { color = 0xFFFF4545; } else if (lockedByExclusivity) { color = 0xFFCC3333; } else if (node == selectedNode) { color = 0xFF999933; } else if (node == hoveredNode) { color = 0xFF777777; } else if (unlocked) { color = 0xFF44CC44; } else { int cost = node.getCost(); if (cost <= 1) { color = 0xFF444444; } else if (cost == 2) { color = 0xFFFF6633; } else if (cost == 3) { color = 0xFF33CCCC; } else { color = 0xFFFFD700; } } return color; } private boolean prepareSkillUnlock(SkillNode target) { clearUnlockHighlights(); for (ResourceLocation parentId : target.getLinks()) { if (SkilltreeClientState.isUnlocked(parentId)) continue; SkillNode parent = SkillNodeRegistry.get(parentId); highlightedNode = parentId; Component parentName = parent != null ? parent.getTitle() : Component.literal(parentId.toString()); unlockFailureMessage = Component.literal("Unlock ") .append(parentName.copy().withStyle(ChatFormatting.YELLOW)) .append(Component.literal(" first — it is required for ")) .append(target.getTitle().copy().withStyle(ChatFormatting.WHITE)); return false; } if (!meetsClientAttributes(target)) { highlightedNode = target.getId(); unlockFailureMessage = Component.literal("Attribute requirements are not met for ") .append(target.getTitle().copy().withStyle(ChatFormatting.YELLOW)); return false; } if (SkilltreeClientState.getCurrentSkillPoints() < Math.max(0, target.getCost())) { highlightSkillPoints = true; unlockFailureMessage = Component.literal("Not enough skill points to unlock ") .append(target.getTitle().copy().withStyle(ChatFormatting.YELLOW)); return false; } return true; } private boolean meetsClientAttributes(SkillNode node) { if (node.getPrereqAttributes().isEmpty()) return true; Minecraft minecraft = Minecraft.getInstance(); if (minecraft.player == null) return false; return minecraft.player.getCapability(PlayerStatsProvider.PLAYER_STATS) .map(stats -> node.getPrereqAttributes().entrySet().stream() .allMatch(entry -> stats.getAttributeLevel(entry.getKey()) >= entry.getValue())) .orElse(false); } private boolean prepareAbilityUnlock(AbilityNode ability) { clearUnlockHighlights(); for (ResourceLocation parentId : ability.getLinks()) { if (SkilltreeClientState.isUnlocked(parentId) || SkilltreeClientState.isAbilityUnlocked(parentId)) continue; SkillNode parentNode = SkillNodeRegistry.get(parentId); if (parentNode != null) { highlightedNode = parentId; } else { highlightedAbility = parentId; } Component parentName = parentNode != null ? parentNode.getTitle() : AbilityNodeRegistry.get(parentId) != null ? AbilityNodeRegistry.get(parentId).getTitle() : Component.literal(parentId.toString()); unlockFailureMessage = Component.literal("Unlock ") .append(parentName.copy().withStyle(ChatFormatting.YELLOW)) .append(Component.literal(" first — it is required for ")) .append(ability.getTitle().copy().withStyle(ChatFormatting.WHITE)); return false; } return true; } private void clearUnlockHighlights() { highlightedNode = null; highlightedAbility = null; highlightSkillPoints = false; unlockFailureMessage = null; } private void renderUnlockFailure(GuiGraphics gfx) { if (unlockFailureMessage == null) return; int messageWidth = Math.min(width - 40, font.width(unlockFailureMessage) + 20); int x = (width - messageWidth) / 2; int y = height - 34; gfx.fill(x - 2, y - 2, x + messageWidth + 2, y + 18, 0xCC000000); gfx.fill(x, y, x + messageWidth, y + 16, 0xE0421717); gfx.fill(x, y, x + 3, y + 16, 0xFFFF4545); gfx.drawCenteredString(font, unlockFailureMessage, width / 2, y + 4, 0xFFFFFF); } private void drawLine(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) { float dx = x2 - x1; float dy = y2 - y1; float length = (float) Math.sqrt(dx * dx + dy * dy); float angle = (float) Math.atan2(dy, dx); gfx.pose().pushPose(); gfx.pose().translate(x1, y1, 0); gfx.pose().mulPose(Axis.ZP.rotation(angle)); gfx.fill( 0, (int) (-LINE_THICKNESS / 2f), (int) length, (int) (LINE_THICKNESS / 2f), color ); gfx.pose().popPose(); } private void drawConnection(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) { drawLine(gfx, x1 + 1, y1 + 2, x2 + 1, y2 + 2, 0x99000000); drawLine(gfx, x1, y1, x2, y2, color); } private Iterable activeSkillNodes() { return activeTree != null ? SkillNodeRegistry.byTree(activeTree) : SkillNodeRegistry.all(); } private void rebuildTreeTabs() { List trees = SkillNodeRegistry.trees(); if (trees.size() <= 1 || selectedNode != null || selectedAbility != null) { return; } int x = 10; int y = 28; for (ResourceLocation tree : trees) { String label = tree.toString(); int buttonWidth = Math.min(140, Math.max(60, font.width(label) + 16)); Button button = addRenderableWidget(Button.builder(Component.literal(label), btn -> { activeTree = tree; selectedNode = null; selectedAbility = null; clearWidgets(); rebuildTreeTabs(); }) .pos(x, y) .size(buttonWidth, 20) .build()); button.active = !tree.equals(activeTree); x += buttonWidth + 4; } } @Override public boolean isPauseScreen() { return false; } }