revamp skilltree visuals
This commit is contained in:
@@ -18,11 +18,11 @@ import java.util.List;
|
||||
public class AbilityBindingScreen extends Screen {
|
||||
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
private static final int PANEL_HEIGHT = 180;
|
||||
private static final int PANEL_PADDING = 10;
|
||||
private static final int PANEL_HEIGHT = 204;
|
||||
private static final int PANEL_PADDING = 16;
|
||||
private static final int SLOT_BUTTON_WIDTH = 76;
|
||||
private static final int BUTTON_HEIGHT = 20;
|
||||
private static final int BUTTON_GAP = 8;
|
||||
private static final int BUTTON_GAP = 6;
|
||||
|
||||
private final AbilityNode ability;
|
||||
|
||||
@@ -43,7 +43,7 @@ public class AbilityBindingScreen extends Screen {
|
||||
int boundSlot = SkilltreeClientState.getSlotOfAbility(ability.getId());
|
||||
for (int slot = 1; slot <= visibleSlots; slot++) {
|
||||
int currentSlot = slot;
|
||||
Button button = addRenderableWidget(Button.builder(Component.literal("Slot " + slot),
|
||||
Button button = addRenderableWidget(SkillUi.button(Component.literal("Slot " + slot),
|
||||
b -> bind(currentSlot))
|
||||
.pos(buttonX + (SLOT_BUTTON_WIDTH + BUTTON_GAP) * (slot - 1), buttonY)
|
||||
.size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT)
|
||||
@@ -53,7 +53,7 @@ public class AbilityBindingScreen extends Screen {
|
||||
button.setMessage(Component.literal("Bound " + slot));
|
||||
}
|
||||
}
|
||||
addRenderableWidget(Button.builder(Component.literal("Cancel"),
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Cancel"),
|
||||
b -> onClose()).pos(x + PANEL_WIDTH / 2 - 40, y + PANEL_HEIGHT - 28).size(80, BUTTON_HEIGHT).build());
|
||||
}
|
||||
|
||||
@@ -84,14 +84,15 @@ public class AbilityBindingScreen extends Screen {
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int y = (height - PANEL_HEIGHT) / 2;
|
||||
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, PANEL_HEIGHT);
|
||||
gfx.drawString(font, "ASSIGN ABILITY", x + 16, y + 14, SkillUi.ACCENT, false);
|
||||
ResourceLocation icon = ability.getIcon();
|
||||
if (icon != null) {
|
||||
gfx.blit(icon, x + PANEL_WIDTH / 2 - 10, y + 10, 0, 0, 20, 20, 20, 20);
|
||||
gfx.blit(icon, x + 16, y + 34, 0, 0, 20, 20, 20, 20);
|
||||
}
|
||||
gfx.drawCenteredString(font, ability.getTitle(), x + PANEL_WIDTH / 2, y + 35, 0xFFFFFF);
|
||||
gfx.drawString(font, font.plainSubstrByWidth(ability.getTitle().getString(), PANEL_WIDTH - 64), x + 44, y + 40, SkillUi.TEXT, false);
|
||||
if (ability.getDescription() != null) {
|
||||
drawDescription(gfx, x + PANEL_PADDING, y + 50, y + PANEL_HEIGHT - 60);
|
||||
drawDescription(gfx, x + PANEL_PADDING, y + 66, y + PANEL_HEIGHT - 60);
|
||||
}
|
||||
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
@@ -109,8 +110,8 @@ public class AbilityBindingScreen extends Screen {
|
||||
if (lineY + font.lineHeight > bottomY) {
|
||||
break;
|
||||
}
|
||||
gfx.drawString(font, line.getString(), x, lineY, 0xDDDDDD);
|
||||
lineY += font.lineHeight;
|
||||
gfx.drawString(font, line.getString(), x, lineY, SkillUi.MUTED);
|
||||
lineY += font.lineHeight + 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ public class AbilityHudOverlay {
|
||||
public static void render(RenderGuiEvent.Post event) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
if (mc.player == null) return;
|
||||
int xStart = event.getGuiGraphics().guiWidth() - 80;
|
||||
int xStart = event.getGuiGraphics().guiWidth() - 124;
|
||||
int yStart = 10;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
@@ -22,13 +22,17 @@ public class AbilityHudOverlay {
|
||||
int cd = SkilltreeClientState.getClientCooldown(slot);
|
||||
ResourceLocation icon = ability.getIcon();
|
||||
GuiGraphics gfx = event.getGuiGraphics();
|
||||
gfx.blit(icon, xStart, yStart, 0, 0, 16, 16, 16, 16);
|
||||
if (cd > 0) {
|
||||
String text = String.valueOf(cd / 20);
|
||||
gfx.drawString(mc.font, text, xStart + 20, yStart + 4, 0xFF5555);
|
||||
gfx.fill(xStart, yStart, xStart + 112, yStart + 26, SkillUi.BACKGROUND);
|
||||
gfx.fill(xStart, yStart, xStart + 2, yStart + 26, cd > 0 ? SkillUi.BORDER : SkillUi.ACCENT);
|
||||
if (icon != null) {
|
||||
gfx.blit(icon, xStart + 7, yStart + 5, 0, 0, 16, 16, 16, 16);
|
||||
}
|
||||
gfx.drawString(mc.font, "Ability " + slot, xStart + 40, yStart + 4, 0xFFFFFF);
|
||||
yStart += 20;
|
||||
if (cd > 0) {
|
||||
String text = String.valueOf((cd + 19) / 20) + "s";
|
||||
gfx.drawString(mc.font, text, xStart + 84, yStart + 9, SkillUi.ACCENT);
|
||||
}
|
||||
gfx.drawString(mc.font, "Ability " + slot, xStart + 30, yStart + 9, SkillUi.TEXT);
|
||||
yStart += 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,12 @@ public class AbilitySelectScreen extends Screen {
|
||||
private final int slot;
|
||||
private final List<AbilityNode> unlockedAbilities = new ArrayList<>();
|
||||
private int page = 0;
|
||||
private static final int PAGE_SIZE = 8;
|
||||
private static final int PANEL_WIDTH = 260;
|
||||
private static final int PANEL_HEIGHT = 220;
|
||||
private int pageSize() {
|
||||
return Math.max(1, Math.min(8, (height - 108) / 28));
|
||||
}
|
||||
|
||||
private int panelHeight() { return 92 + pageSize() * 28; }
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
|
||||
public AbilitySelectScreen(Screen parent, int slot) {
|
||||
super(Component.literal("Select Ability"));
|
||||
@@ -38,46 +41,47 @@ public class AbilitySelectScreen extends Screen {
|
||||
unlockedAbilities.add(node);
|
||||
}
|
||||
}
|
||||
page = Math.min(page, Math.max(0, (unlockedAbilities.size() - 1) / pageSize()));
|
||||
buildPageButtons();
|
||||
}
|
||||
|
||||
private void buildPageButtons() {
|
||||
clearWidgets();
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int y = (height - PANEL_HEIGHT) / 2;
|
||||
int y = (height - panelHeight()) / 2;
|
||||
|
||||
int start = page * PAGE_SIZE;
|
||||
int end = Math.min(unlockedAbilities.size(), start + PAGE_SIZE);
|
||||
int rowY = y + 40;
|
||||
int start = page * pageSize();
|
||||
int end = Math.min(unlockedAbilities.size(), start + pageSize());
|
||||
int rowY = y + 52;
|
||||
for (int i = start; i < end; i++) {
|
||||
AbilityNode node = unlockedAbilities.get(i);
|
||||
String label = node.getTitle().getString();
|
||||
boolean alreadyInThisSlot = SkilltreeClientState.getSlotOfAbility(node.getId()) == slot;
|
||||
Button btn = Button.builder(Component.literal(label), b -> select(node))
|
||||
.pos(x + 15, rowY)
|
||||
.size(PANEL_WIDTH - 30, 20)
|
||||
Button btn = SkillUi.button(Component.literal(label), b -> select(node))
|
||||
.pos(x + 16, rowY)
|
||||
.size(PANEL_WIDTH - 32, 24)
|
||||
.build();
|
||||
if (alreadyInThisSlot) {
|
||||
btn.active = false;
|
||||
btn.setMessage(Component.literal(label + " (bound)"));
|
||||
}
|
||||
addRenderableWidget(btn);
|
||||
rowY += 24;
|
||||
rowY += 28;
|
||||
}
|
||||
if (page > 0) {
|
||||
addRenderableWidget(Button.builder(Component.literal("< Prev"), b -> {
|
||||
addRenderableWidget(SkillUi.button(Component.literal("< Prev"), b -> {
|
||||
page--;
|
||||
buildPageButtons();
|
||||
}).pos(x + 15, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||
}).pos(x + 16, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
if ((page + 1) * PAGE_SIZE < unlockedAbilities.size()) {
|
||||
addRenderableWidget(Button.builder(Component.literal("Next >"), b -> {
|
||||
if ((page + 1) * pageSize() < unlockedAbilities.size()) {
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Next >"), b -> {
|
||||
page++;
|
||||
buildPageButtons();
|
||||
}).pos(x + 80, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||
}).pos(x + 80, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
addRenderableWidget(Button.builder(Component.literal("Back"), b -> back())
|
||||
.pos(x + PANEL_WIDTH - 70, y + PANEL_HEIGHT - 28).size(60, 20).build());
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Back"), b -> back())
|
||||
.pos(x + PANEL_WIDTH - 70, y + panelHeight() - 28).size(60, 20).build());
|
||||
}
|
||||
|
||||
private void select(AbilityNode node) {
|
||||
@@ -105,11 +109,14 @@ public class AbilitySelectScreen extends Screen {
|
||||
public void render(@NotNull GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||
this.renderBackground(gfx);
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int y = (height - PANEL_HEIGHT) / 2;
|
||||
int y = (height - panelHeight()) / 2;
|
||||
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
|
||||
String title = "Select for slot " + (slot == 1 ? "Y" : slot == 2 ? "X" : "C");
|
||||
gfx.drawCenteredString(font, Component.literal(title), x + PANEL_WIDTH / 2, y + 12, 0xFFFFFF);
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, panelHeight());
|
||||
SkillUi.heading(gfx, font, "CHOOSE ABILITY", "Slot " + slot + " / Page " + (page + 1)
|
||||
+ " of " + Math.max(1, (unlockedAbilities.size() + pageSize() - 1) / pageSize()), x, y);
|
||||
if (unlockedAbilities.isEmpty()) {
|
||||
gfx.drawString(font, "Unlock an ability in the skill tree.", x + 16, y + 58, SkillUi.MUTED, false);
|
||||
}
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,16 @@ import net.fretux.skillengine.skilltree.AbilityNode;
|
||||
import net.fretux.skillengine.skilltree.AbilityNodeRegistry;
|
||||
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.resources.ResourceLocation;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class AbilitySlotsScreen extends Screen {
|
||||
private static final int PANEL_WIDTH = 260;
|
||||
private static final int MIN_PANEL_HEIGHT = 180;
|
||||
private static final int PANEL_WIDTH = 280;
|
||||
private static final int MIN_PANEL_HEIGHT = 204;
|
||||
private static final int SLOT_TEXT_RIGHT_PADDING = 8;
|
||||
private static final int REBIND_BUTTON_WIDTH = 80;
|
||||
private static final int REBIND_BUTTON_WIDTH = 62;
|
||||
public AbilitySlotsScreen() {
|
||||
super(Component.literal("Abilities"));
|
||||
}
|
||||
@@ -24,16 +23,16 @@ public class AbilitySlotsScreen extends Screen {
|
||||
int x = (width - PANEL_WIDTH) / 2;
|
||||
int panelHeight = panelHeight();
|
||||
int y = (height - panelHeight) / 2;
|
||||
int rowY = y + 40;
|
||||
int buttonH = 20;
|
||||
int buttonX = x + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 15;
|
||||
int rowY = y + 52;
|
||||
int buttonH = 28;
|
||||
int buttonX = x + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 16;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
int currentSlot = slot;
|
||||
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(currentSlot))
|
||||
.pos(buttonX, rowY + (slot - 1) * 35).size(REBIND_BUTTON_WIDTH, buttonH).build());
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Rebind"), b -> openSelect(currentSlot))
|
||||
.pos(buttonX, rowY + (slot - 1) * 40).size(REBIND_BUTTON_WIDTH, buttonH).build());
|
||||
}
|
||||
addRenderableWidget(Button.builder(Component.literal("Close"), b -> onClose())
|
||||
addRenderableWidget(SkillUi.button(Component.literal("Close"), b -> onClose())
|
||||
.pos(x + PANEL_WIDTH - 70, y + panelHeight - 28).size(60, 20).build());
|
||||
}
|
||||
|
||||
@@ -58,19 +57,19 @@ public class AbilitySlotsScreen extends Screen {
|
||||
int panelHeight = panelHeight();
|
||||
int y = (height - panelHeight) / 2;
|
||||
gfx.fill(0, 0, width, height, 0xAA000000);
|
||||
gfx.fill(x, y, x + PANEL_WIDTH, y + panelHeight, 0xFF222222);
|
||||
gfx.drawCenteredString(font, Component.literal("Ability Slots"), x + PANEL_WIDTH / 2, y + 12, 0xFFFFFF);
|
||||
int rowY = y + 40;
|
||||
SkillUi.panel(gfx, x, y, PANEL_WIDTH, panelHeight);
|
||||
SkillUi.heading(gfx, font, "ABILITY LOADOUT", "Prepare your active skills", x, y);
|
||||
int rowY = y + 52;
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
for (int slot = 1; slot <= slots; slot++) {
|
||||
drawSlotRow(gfx, x + 15, rowY + (slot - 1) * 35, slot, "Slot " + slot);
|
||||
drawSlotRow(gfx, x + 16, rowY + (slot - 1) * 40, slot, "Slot " + slot);
|
||||
}
|
||||
super.render(gfx, mouseX, mouseY, partialTick);
|
||||
}
|
||||
|
||||
private void drawSlotRow(GuiGraphics gfx, int x, int y, int slot, String label) {
|
||||
String slotLabel = label + ":";
|
||||
gfx.drawString(font, slotLabel, x, y + 6, 0xFFFFFF);
|
||||
gfx.fill(x, y - 2, x + PANEL_WIDTH - 32, y + 32, SkillUi.SURFACE);
|
||||
gfx.drawString(font, label.toUpperCase(java.util.Locale.ROOT), x + 8, y + 2, SkillUi.MUTED, false);
|
||||
ResourceLocation abilityId = SkilltreeClientState.getAbilityInSlot(slot);
|
||||
String title = "Empty";
|
||||
ResourceLocation icon = null;
|
||||
@@ -84,21 +83,22 @@ public class AbilitySlotsScreen extends Screen {
|
||||
}
|
||||
}
|
||||
if (icon != null) {
|
||||
int iconX = x + font.width(slotLabel) + 6;
|
||||
gfx.blit(icon, iconX, y - 2, 0, 0, 20, 20, 20, 20);
|
||||
int textX = iconX + 26;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 6, 0xDDDDDD);
|
||||
int iconX = x + 8;
|
||||
gfx.blit(icon, iconX, y + 13, 0, 0, 16, 16, 16, 16);
|
||||
int textX = iconX + 22;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 17, SkillUi.TEXT);
|
||||
} else {
|
||||
int textX = x + font.width(slotLabel) + 6;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 6, 0x888888);
|
||||
int textX = x + 8;
|
||||
gfx.drawString(font, fitTitle(title, textX), textX, y + 17, SkillUi.MUTED);
|
||||
}
|
||||
}
|
||||
|
||||
private String fitTitle(String title, int textX) {
|
||||
int panelX = (width - PANEL_WIDTH) / 2;
|
||||
int buttonX = panelX + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 15;
|
||||
int buttonX = panelX + PANEL_WIDTH - REBIND_BUTTON_WIDTH - 16;
|
||||
int availableWidth = buttonX - SLOT_TEXT_RIGHT_PADDING - textX;
|
||||
if (availableWidth <= 0 || font.width(title) <= availableWidth) {
|
||||
if (availableWidth <= 0) return "";
|
||||
if (font.width(title) <= availableWidth) {
|
||||
return title;
|
||||
}
|
||||
String suffix = "...";
|
||||
@@ -111,6 +111,6 @@ public class AbilitySlotsScreen extends Screen {
|
||||
|
||||
private int panelHeight() {
|
||||
int slots = SkilltreeClientState.getAbilitySlots().length;
|
||||
return Math.max(MIN_PANEL_HEIGHT, 80 + slots * 35);
|
||||
return Math.max(MIN_PANEL_HEIGHT, 92 + slots * 40);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.fretux.skillengine.client;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.Font;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
/** Shared charcoal and iron surfaces for the skill interface. */
|
||||
final class SkillUi {
|
||||
static final int BACKGROUND = 0xFF181A1D;
|
||||
static final int SURFACE = 0xFF22252A;
|
||||
static final int HEADER = 0xFF292D32;
|
||||
static final int BORDER = 0xFF555B63;
|
||||
static final int ACCENT = 0xFFD0A447;
|
||||
static final int TEXT = 0xFFE7E7E7;
|
||||
static final int MUTED = 0xFFA9ADB2;
|
||||
|
||||
private SkillUi() {}
|
||||
|
||||
static void panel(GuiGraphics gfx, int x, int y, int width, int height) {
|
||||
gfx.fill(x + 3, y + 4, x + width + 3, y + height + 4, 0x66000000);
|
||||
gfx.fill(x - 1, y - 1, x + width + 1, y + height + 1, BORDER);
|
||||
gfx.fill(x, y, x + width, y + height, BACKGROUND);
|
||||
}
|
||||
|
||||
static void heading(GuiGraphics gfx, Font font, String title, String subtitle, int x, int y) {
|
||||
gfx.drawString(font, title, x + 16, y + 14, TEXT, false);
|
||||
gfx.drawString(font, subtitle, x + 16, y + 29, MUTED, false);
|
||||
}
|
||||
|
||||
static Builder button(Component label, Button.OnPress action) {
|
||||
return new Builder(label, action);
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
private final Component label;
|
||||
private final Button.OnPress action;
|
||||
private int x, y, width = 80, height = 20;
|
||||
|
||||
Builder(Component label, Button.OnPress action) {
|
||||
this.label = label;
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
Builder pos(int x, int y) { this.x = x; this.y = y; return this; }
|
||||
Builder size(int width, int height) { this.width = width; this.height = height; return this; }
|
||||
Button build() { return new IronButton(x, y, width, height, label, action); }
|
||||
}
|
||||
|
||||
private static final class IronButton extends Button {
|
||||
IronButton(int x, int y, int width, int height, Component label, OnPress action) {
|
||||
super(x, y, width, height, label, action, DEFAULT_NARRATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renderWidget(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) {
|
||||
boolean focus = active && isHoveredOrFocused();
|
||||
int x = getX(), y = getY();
|
||||
gfx.fill(x, y, x + width, y + height, focus ? MUTED : BORDER);
|
||||
gfx.fill(x + 1, y + 1, x + width - 1, y + height - 1,
|
||||
active ? (focus ? HEADER : SURFACE) : BACKGROUND);
|
||||
renderScrollingString(gfx, Minecraft.getInstance().font, 6, active ? TEXT : MUTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@ 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.Util;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.Button;
|
||||
import net.minecraft.client.gui.components.Tooltip;
|
||||
import net.minecraft.client.gui.screens.Screen;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.FormattedText;
|
||||
@@ -19,22 +21,39 @@ import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.Mth;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
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 int OVERLAY_WIDTH = 280;
|
||||
private static final int UNAVAILABLE = 0xFF777D85;
|
||||
private static final int AVAILABLE = 0xFFD0A447;
|
||||
private static final int UNLOCKED = 0xFF72B77A;
|
||||
private static final int BLOCKED = 0xFFD16A65;
|
||||
private static final int LOCKED_PATH = 0xFF383D43;
|
||||
|
||||
private record PathNode(ResourceLocation id, boolean ability) {}
|
||||
private record Connection(int x1, int y1, int x2, int y2, int color, boolean traced) {}
|
||||
private Button unlockButton;
|
||||
private Button inspectorCloseButton;
|
||||
private long inspectorOpenedAt;
|
||||
private int inspectorSlideOffset;
|
||||
private int inspectorAnimationStartOffset;
|
||||
private boolean inspectorClosing;
|
||||
private static final float INSPECTOR_SLIDE_MS = 180.0f;
|
||||
private boolean legendOpen;
|
||||
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 int detailScroll;
|
||||
private int maxDetailScroll;
|
||||
private double panX = 0;
|
||||
private double panY = 0;
|
||||
private boolean dragging = false;
|
||||
@@ -59,11 +78,18 @@ public class SkilltreeScreen extends Screen {
|
||||
List<ResourceLocation> trees = SkillNodeRegistry.trees();
|
||||
activeTree = trees.isEmpty() ? null : trees.get(0);
|
||||
}
|
||||
rebuildTreeTabs();
|
||||
if (selectedNode != null) rebuildOverlayButtons();
|
||||
else if (selectedAbility != null) rebuildAbilityOverlayButtons();
|
||||
else rebuildTreeTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean mouseScrolled(double mouseX, double mouseY, double delta) {
|
||||
if (legendOpen && isOverLegend(mouseX, mouseY)) return true;
|
||||
if (hasSelection() && isOverInspector(mouseX, mouseY)) {
|
||||
detailScroll = Mth.clamp(detailScroll - (int) (delta * 12), 0, maxDetailScroll);
|
||||
return true;
|
||||
}
|
||||
zoom += (float) (delta * ZOOM_STEP);
|
||||
float minZoom = 0.5f;
|
||||
float maxZoom = 2.0f;
|
||||
@@ -73,46 +99,49 @@ public class SkilltreeScreen extends Screen {
|
||||
|
||||
@Override
|
||||
public boolean mouseClicked(double mouseX, double mouseY, int button) {
|
||||
if (selectedNode != null || selectedAbility != null) {
|
||||
return super.mouseClicked(mouseX, mouseY, button);
|
||||
}
|
||||
if (super.mouseClicked(mouseX, mouseY, button)) return true;
|
||||
if (legendOpen && isOverLegend(mouseX, mouseY)) return true;
|
||||
if (hasSelection() && isOverInspector(mouseX, mouseY)) return true;
|
||||
if (children().stream().anyMatch(child -> child.isMouseOver(mouseX, mouseY))) return true;
|
||||
if (mouseY < 36 || mouseY >= height - 23) return false;
|
||||
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())) {
|
||||
if (clickedNode == selectedNode) {
|
||||
closeInspector();
|
||||
return true;
|
||||
}
|
||||
selectedAbility = null;
|
||||
selectedNode = clickedNode;
|
||||
rebuildOverlayButtons();
|
||||
revealSelection(clickedNode.getX(), clickedNode.getY());
|
||||
return true;
|
||||
}
|
||||
AbilityNode clickedAbility = findAbilityAt(mouseX, mouseY);
|
||||
if (clickedAbility != null) {
|
||||
if (clickedAbility == selectedAbility) {
|
||||
closeInspector();
|
||||
return true;
|
||||
}
|
||||
selectedNode = null;
|
||||
if (SkilltreeClientState.isAbilityUnlocked(clickedAbility.getId())) {
|
||||
Minecraft.getInstance().setScreen(new AbilityBindingScreen(clickedAbility));
|
||||
} else {
|
||||
selectedAbility = clickedAbility;
|
||||
rebuildAbilityOverlayButtons();
|
||||
revealSelection(clickedAbility.getX(), clickedAbility.getY());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (hasSelection()) closeInspector();
|
||||
dragging = true;
|
||||
return 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;
|
||||
@@ -131,12 +160,19 @@ public class SkilltreeScreen extends Screen {
|
||||
|
||||
@Override
|
||||
public void render(@NotNull GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) {
|
||||
updateInspectorAnimation();
|
||||
this.renderBackground(gfx);
|
||||
renderTreeBackdrop(gfx);
|
||||
renderSkillPointHeader(gfx);
|
||||
hoveredNode = findNodeAt(mouseX, mouseY);
|
||||
hoveredAbility = findAbilityAt(mouseX, mouseY);
|
||||
boolean overCanvas = mouseY >= 36 && mouseY < height - 23
|
||||
&& !(legendOpen && isOverLegend(mouseX, mouseY))
|
||||
&& !(hasSelection() && isOverInspector(mouseX, mouseY))
|
||||
&& children().stream().noneMatch(child -> child.isMouseOver(mouseX, mouseY));
|
||||
hoveredNode = overCanvas ? findNodeAt(mouseX, mouseY) : null;
|
||||
hoveredAbility = overCanvas ? findAbilityAt(mouseX, mouseY) : null;
|
||||
gfx.enableScissor(0, 36, width, height - 23);
|
||||
drawGraph(gfx);
|
||||
gfx.disableScissor();
|
||||
renderSkillPointHeader(gfx);
|
||||
renderUnlockFailure(gfx);
|
||||
if (hoveredNode != null && selectedNode == null && selectedAbility == null) {
|
||||
renderNodeTooltip(gfx, hoveredNode, mouseX, mouseY);
|
||||
@@ -147,43 +183,102 @@ public class SkilltreeScreen extends Screen {
|
||||
renderAbilityOverlay(gfx, selectedAbility);
|
||||
}
|
||||
super.render(gfx, mouseX, mouseY, partialTicks);
|
||||
if (legendOpen) renderLegend(gfx);
|
||||
}
|
||||
|
||||
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.fill(0, 0, width, 36, SkillUi.BACKGROUND);
|
||||
gfx.fill(0, 35, width, 36, SkillUi.BORDER);
|
||||
String points = "Skill Points: " + SkilltreeClientState.getCurrentSkillPoints();
|
||||
int pointsX = width - font.width(points) - 32;
|
||||
gfx.fill(pointsX - 1, 7, width - 11, 29, highlightSkillPoints ? BLOCKED : SkillUi.BORDER);
|
||||
gfx.fill(pointsX, 8, width - 12, 28, SkillUi.SURFACE);
|
||||
gfx.drawString(font, points, pointsX + 10, 14, SkillUi.TEXT, false);
|
||||
gfx.fill(0, height - 23, width, height, SkillUi.BACKGROUND);
|
||||
gfx.drawString(font, "Legend", 38, height - 15, SkillUi.MUTED, false);
|
||||
String zoomLabel = Math.round(zoom * 100) + "%";
|
||||
gfx.drawString(font, zoomLabel, width - font.width(zoomLabel) - 14, height - 15, SkillUi.ACCENT, false);
|
||||
}
|
||||
|
||||
private static String displayName(String value) {
|
||||
StringBuilder name = new StringBuilder();
|
||||
for (String word : value.replaceAll("[_/:.\\-]+", " ").split(" +")) {
|
||||
if (word.isEmpty()) continue;
|
||||
if (!name.isEmpty()) name.append(' ');
|
||||
name.append(Character.toUpperCase(word.charAt(0))).append(word.substring(1));
|
||||
}
|
||||
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);
|
||||
return name.toString();
|
||||
}
|
||||
|
||||
private void renderInspectorHeading(GuiGraphics gfx, Component title, ResourceLocation icon) {
|
||||
int x = inspectorX() + 12;
|
||||
int y = inspectorY();
|
||||
if (icon != null) {
|
||||
gfx.blit(icon, x, y + 5, NODE_ICON_SIZE, NODE_ICON_SIZE, 0, 0,
|
||||
NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE, NODE_ICON_SIZE);
|
||||
x += NODE_ICON_SIZE + 6;
|
||||
}
|
||||
int availableWidth = inspectorX() + inspectorWidth() - 30 - x;
|
||||
gfx.drawString(font, font.plainSubstrByWidth(title.getString(), availableWidth),
|
||||
x, y + 10, SkillUi.TEXT, false);
|
||||
}
|
||||
|
||||
private Set<PathNode> prerequisiteChain(PathNode focus) {
|
||||
Set<PathNode> visited = new HashSet<>();
|
||||
if (focus == null) return visited;
|
||||
ArrayDeque<PathNode> pending = new ArrayDeque<>();
|
||||
pending.add(focus);
|
||||
while (!pending.isEmpty()) {
|
||||
PathNode current = pending.removeFirst();
|
||||
if (!visited.add(current)) continue;
|
||||
SkillNode node = current.ability() ? null : SkillNodeRegistry.get(current.id());
|
||||
AbilityNode ability = current.ability() ? AbilityNodeRegistry.get(current.id()) : null;
|
||||
List<ResourceLocation> parents = node != null ? node.getLinks()
|
||||
: ability != null ? ability.getLinks() : List.of();
|
||||
for (ResourceLocation id : parents) {
|
||||
if (SkillNodeRegistry.get(id) != null) pending.add(new PathNode(id, false));
|
||||
else if (current.ability() && AbilityNodeRegistry.get(id) != null) {
|
||||
pending.add(new PathNode(id, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
return visited;
|
||||
}
|
||||
|
||||
private int pathEmphasis(int color, boolean focused, boolean traced) {
|
||||
if (traced) {
|
||||
if (color == LOCKED_PATH) return SkillUi.MUTED;
|
||||
return mixColor(color, SkillUi.TEXT, 0.2f);
|
||||
}
|
||||
return focused ? mixColor(color, SkillUi.BACKGROUND, 0.45f) : color;
|
||||
}
|
||||
|
||||
private static int mixColor(int from, int to, float amount) {
|
||||
int red = (int) (((from >> 16) & 255) * (1 - amount) + ((to >> 16) & 255) * amount);
|
||||
int green = (int) (((from >> 8) & 255) * (1 - amount) + ((to >> 8) & 255) * amount);
|
||||
int blue = (int) ((from & 255) * (1 - amount) + (to & 255) * amount);
|
||||
return 0xFF000000 | red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private void renderTreeBackdrop(GuiGraphics gfx) {
|
||||
gfx.fill(0, 0, width, height, 0xD00B0E14);
|
||||
gfx.fill(0, 0, width, height, SkillUi.BACKGROUND);
|
||||
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(x, y, x + 1, y + 1, 0x30555B63);
|
||||
}
|
||||
}
|
||||
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) {
|
||||
PathNode focus = hoveredNode != null ? new PathNode(hoveredNode.getId(), false)
|
||||
: hoveredAbility != null ? new PathNode(hoveredAbility.getId(), true)
|
||||
: selectedNode != null ? new PathNode(selectedNode.getId(), false)
|
||||
: selectedAbility != null ? new PathNode(selectedAbility.getId(), true) : null;
|
||||
Set<PathNode> chain = prerequisiteChain(focus);
|
||||
List<Connection> connections = new ArrayList<>();
|
||||
for (SkillNode node : activeSkillNodes()) {
|
||||
int[] p1 = worldToScreen(node.getX(), node.getY());
|
||||
for (ResourceLocation neighborId : node.getLinks()) {
|
||||
@@ -193,8 +288,13 @@ public class SkilltreeScreen extends Screen {
|
||||
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);
|
||||
int linkColor = bothUnlocked ? UNLOCKED
|
||||
: isLockedByExclusivity(node) ? BLOCKED
|
||||
: getNodeColor(node) == AVAILABLE && SkilltreeClientState.isUnlocked(neighborId)
|
||||
? AVAILABLE : LOCKED_PATH;
|
||||
boolean traced = chain.contains(new PathNode(node.getId(), false));
|
||||
connections.add(new Connection(p1[0], p1[1], p2[0], p2[1],
|
||||
pathEmphasis(linkColor, focus != null, traced), traced));
|
||||
}
|
||||
}
|
||||
for (AbilityNode ability : AbilityNodeRegistry.all()) {
|
||||
@@ -204,31 +304,32 @@ public class SkilltreeScreen extends Screen {
|
||||
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);
|
||||
boolean traced = chain.contains(new PathNode(ability.getId(), true));
|
||||
connections.add(new Connection(p1[0], p1[1], p2[0], p2[1],
|
||||
pathEmphasis(abilityPathColor(ability, link), focus != null, traced), traced));
|
||||
} else if (ability2 != null) {
|
||||
int[] p2 = worldToScreen(ability2.getX(), ability2.getY());
|
||||
drawConnection(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6579C7);
|
||||
boolean traced = chain.contains(new PathNode(ability.getId(), true));
|
||||
connections.add(new Connection(p1[0], p1[1], p2[0], p2[1],
|
||||
pathEmphasis(abilityPathColor(ability, link), focus != null, traced), traced));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Draw the traced chain last so unrelated crossing lines cannot obscure it.
|
||||
for (boolean traced : new boolean[]{false, true}) {
|
||||
for (Connection connection : connections) {
|
||||
if (connection.traced() != traced) continue;
|
||||
drawConnection(gfx, connection.x1(), connection.y1(), connection.x2(), connection.y2(), connection.color());
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
int color = SkilltreeClientState.isAbilityUnlocked(ability.getId()) ? UNLOCKED
|
||||
: abilityRequirementsMet(ability) ? AVAILABLE : UNAVAILABLE;
|
||||
renderNodeFrame(gfx, pos[0], pos[1], radius, color,
|
||||
ability == hoveredAbility, ability == selectedAbility, true);
|
||||
ability == hoveredAbility || ability.getId().equals(highlightedAbility),
|
||||
ability == selectedAbility);
|
||||
ResourceLocation icon = ability.getIcon();
|
||||
if (icon != null) {
|
||||
int size = getRenderedNodeIconSize();
|
||||
@@ -242,7 +343,7 @@ public class SkilltreeScreen extends Screen {
|
||||
int radius = getRenderedNodeRadius(NODE_RADIUS);
|
||||
int color = getNodeColor(node);
|
||||
renderNodeFrame(gfx, pos[0], pos[1], radius, color,
|
||||
node == hoveredNode, node == selectedNode, false);
|
||||
node == hoveredNode || node.getId().equals(highlightedNode), node == selectedNode);
|
||||
ResourceLocation icon = node.getIcons();
|
||||
if (icon != null) {
|
||||
int size = getRenderedNodeIconSize();
|
||||
@@ -256,28 +357,43 @@ public class SkilltreeScreen extends Screen {
|
||||
NODE_ICON_SIZE, NODE_ICON_SIZE
|
||||
);
|
||||
}
|
||||
renderCostBadge(gfx, pos[0], pos[1], radius, node.getCost());
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCostBadge(GuiGraphics gfx, int cx, int cy, int radius, int cost) {
|
||||
if (cost <= 1) return;
|
||||
String label = Integer.toString(cost);
|
||||
float scale = 0.75f;
|
||||
gfx.pose().pushPose();
|
||||
gfx.pose().translate(cx + radius - 3, cy + radius - 3, 0);
|
||||
gfx.pose().scale(scale, scale, 1);
|
||||
int badgeWidth = font.width(label);
|
||||
gfx.fill(-2, -1, badgeWidth + 2, font.lineHeight, SkillUi.BACKGROUND);
|
||||
gfx.drawString(font, label, 0, 0, SkillUi.TEXT, false);
|
||||
gfx.pose().popPose();
|
||||
}
|
||||
|
||||
private void renderNodeFrame(GuiGraphics gfx, int cx, int cy, int radius, int color,
|
||||
boolean hovered, boolean selected, boolean ability) {
|
||||
int glow = ability ? 0x554B79D8 : 0x554FCB72;
|
||||
boolean hovered, boolean selected) {
|
||||
if (hovered || selected) {
|
||||
int spread = selected ? 5 : 3;
|
||||
int spread = selected ? 4 : 3;
|
||||
gfx.fill(cx - radius - spread, cy - radius - spread,
|
||||
cx + radius + spread, cy + radius + spread, glow);
|
||||
cx + radius + spread, cy + radius + spread, SkillUi.TEXT);
|
||||
gfx.fill(cx - radius - spread + 1, cy - radius - spread + 1,
|
||||
cx + radius + spread - 1, cy + radius + spread - 1, SkillUi.BACKGROUND);
|
||||
}
|
||||
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);
|
||||
gfx.fill(cx - radius - 1, cy - radius - 1, cx + radius + 1, cy + radius + 1, color);
|
||||
gfx.fill(cx - radius + 1, cy - radius + 1, cx + radius - 1, cy + radius - 1, SkillUi.SURFACE);
|
||||
}
|
||||
|
||||
private void renderNodeTooltip(GuiGraphics gfx, SkillNode node, int mouseX, int mouseY) {
|
||||
List<Component> tooltip = new ArrayList<>();
|
||||
tooltip.add(node.getTitle());
|
||||
String status = skillStatus(node);
|
||||
if (!status.equals("Not enough skill points")) {
|
||||
tooltip.add(Component.literal(status).withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
String desc = node.getDescription().getString();
|
||||
List<FormattedText> wrapped = font.getSplitter().splitLines(
|
||||
desc,
|
||||
@@ -288,9 +404,9 @@ public class SkilltreeScreen extends Screen {
|
||||
tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY));
|
||||
}
|
||||
tooltip.add(Component.literal("Cost: " + node.getCost())
|
||||
.withStyle(ChatFormatting.DARK_AQUA));
|
||||
.withStyle(ChatFormatting.GRAY));
|
||||
if (!node.getPrereqAttributes().isEmpty()) {
|
||||
tooltip.add(Component.literal("Requirements:")
|
||||
tooltip.add(Component.literal("Stat Requirements:")
|
||||
.withStyle(ChatFormatting.GOLD));
|
||||
assert Minecraft.getInstance().player != null;
|
||||
Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS)
|
||||
@@ -298,7 +414,7 @@ public class SkilltreeScreen extends Screen {
|
||||
int current = stats.getAttributeLevel(attr);
|
||||
boolean ok = current >= required;
|
||||
tooltip.add(
|
||||
Component.literal(" - " + attr + ": " + current + "/" + required)
|
||||
Component.literal(" - " + displayName(attr) + ": " + current + "/" + required)
|
||||
.withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED)
|
||||
);
|
||||
}));
|
||||
@@ -317,21 +433,19 @@ public class SkilltreeScreen extends Screen {
|
||||
}
|
||||
|
||||
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);
|
||||
int x = inspectorX();
|
||||
int y = inspectorY();
|
||||
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};
|
||||
renderInspectorHeading(gfx, node.getTitle(), node.getIcons());
|
||||
beginDetailBody(gfx, x, y);
|
||||
gfx.drawWordWrap(font, node.getDescription(), x + 16, y + 32, inspectorWidth() - 32, SkillUi.MUTED);
|
||||
int descriptionHeight = font.split(node.getDescription(), inspectorWidth() - 32).size() * font.lineHeight;
|
||||
final int[] textY = {y + 32 + descriptionHeight + 16};
|
||||
gfx.drawString(
|
||||
font,
|
||||
Component.literal("Cost: " + node.getCost() + " skill points")
|
||||
.withStyle(ChatFormatting.AQUA),
|
||||
x + 10,
|
||||
.withStyle(ChatFormatting.GRAY),
|
||||
x + 16,
|
||||
textY[0],
|
||||
0xAAAAAA
|
||||
);
|
||||
@@ -339,9 +453,9 @@ public class SkilltreeScreen extends Screen {
|
||||
if (!node.getPrereqAttributes().isEmpty()) {
|
||||
gfx.drawString(
|
||||
font,
|
||||
Component.literal("Requirements:")
|
||||
Component.literal("Stat Requirements:")
|
||||
.withStyle(ChatFormatting.GOLD),
|
||||
x + 10,
|
||||
x + 16,
|
||||
textY[0],
|
||||
0xFFFFFF
|
||||
);
|
||||
@@ -353,135 +467,276 @@ public class SkilltreeScreen extends Screen {
|
||||
boolean ok = current >= required;
|
||||
|
||||
Component line = Component.literal(
|
||||
" - " + attr + ": " + current + "/" + required
|
||||
" - " + displayName(attr) + ": " + current + "/" + required
|
||||
).withStyle(ok ? ChatFormatting.GREEN : ChatFormatting.RED);
|
||||
|
||||
gfx.drawString(font, line, x + 10, textY[0], 0xFFFFFF);
|
||||
textY[0] += 12;
|
||||
gfx.drawWordWrap(font, line, x + 16, textY[0], inspectorWidth() - 32, SkillUi.TEXT);
|
||||
textY[0] += font.split(line, inspectorWidth() - 32).size() * font.lineHeight + 3;
|
||||
}));
|
||||
}
|
||||
textY[0] = renderPrerequisites(gfx, node.getLinks(), x, textY[0], false);
|
||||
if (!node.getExclusiveWith().isEmpty()) {
|
||||
gfx.drawString(font, "Mutually exclusive with:", x + 16, textY[0], SkillUi.MUTED, false);
|
||||
textY[0] += 12;
|
||||
for (ResourceLocation id : node.getExclusiveWith()) {
|
||||
SkillNode other = SkillNodeRegistry.get(id);
|
||||
Component name = other == null ? Component.literal(id.toString()) : other.getTitle();
|
||||
gfx.drawWordWrap(font, name, x + 16, textY[0], inspectorWidth() - 32,
|
||||
SkilltreeClientState.isUnlocked(id) ? BLOCKED : SkillUi.MUTED);
|
||||
textY[0] += font.split(name, inspectorWidth() - 32).size() * font.lineHeight + 3;
|
||||
}
|
||||
}
|
||||
endDetailBody(gfx, y, textY[0]);
|
||||
updateUnlockButton(skillStatus(node), getNodeColor(node) == AVAILABLE);
|
||||
}
|
||||
|
||||
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);
|
||||
int x = inspectorX();
|
||||
int y = inspectorY();
|
||||
renderOverlayPanel(gfx, x, y);
|
||||
gfx.drawCenteredString(font, ability.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF);
|
||||
int textY = y + 30;
|
||||
renderInspectorHeading(gfx, ability.getTitle(), ability.getIcon());
|
||||
beginDetailBody(gfx, x, y);
|
||||
int textY = y + 32;
|
||||
if (ability.getDescription() != null) {
|
||||
gfx.drawWordWrap(font, ability.getDescription(), x + 10, textY, OVERLAY_WIDTH - 20, 0xDDDDDD);
|
||||
textY += 60;
|
||||
gfx.drawWordWrap(font, ability.getDescription(), x + 16, textY, inspectorWidth() - 32, 0xDDDDDD);
|
||||
textY += font.split(ability.getDescription(), inspectorWidth() - 32).size() * font.lineHeight + 16;
|
||||
}
|
||||
gfx.drawString(font,
|
||||
Component.literal("Cooldown: " + ability.getCooldown() / 20 + " seconds")
|
||||
.withStyle(ChatFormatting.AQUA),
|
||||
x + 10,
|
||||
.withStyle(ChatFormatting.GRAY),
|
||||
x + 16,
|
||||
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;
|
||||
}
|
||||
textY = renderPrerequisites(gfx, ability.getLinks(), x, textY, true);
|
||||
endDetailBody(gfx, y, textY);
|
||||
boolean unlocked = SkilltreeClientState.isAbilityUnlocked(ability.getId());
|
||||
updateUnlockButton(unlocked ? "Already unlocked"
|
||||
: abilityRequirementsMet(ability) ? "Ready to unlock" : "Unlock prerequisites first",
|
||||
!unlocked && abilityRequirementsMet(ability));
|
||||
}
|
||||
|
||||
private void beginDetailBody(GuiGraphics gfx, int x, int y) {
|
||||
gfx.enableScissor(x + 16, y + 30, x + inspectorWidth() - 16, y + inspectorHeight() - 40);
|
||||
gfx.pose().pushPose();
|
||||
gfx.pose().translate(0, -detailScroll, 0);
|
||||
}
|
||||
|
||||
private void endDetailBody(GuiGraphics gfx, int y, int bottom) {
|
||||
gfx.pose().popPose();
|
||||
gfx.disableScissor();
|
||||
maxDetailScroll = Math.max(0, bottom - (y + inspectorHeight() - 44));
|
||||
detailScroll = Math.min(detailScroll, maxDetailScroll);
|
||||
if (maxDetailScroll > 0) {
|
||||
int trackX = inspectorX() + inspectorWidth() - 9;
|
||||
int trackHeight = inspectorHeight() - 70;
|
||||
int thumbY = y + 30 + (trackHeight - 16) * detailScroll / maxDetailScroll;
|
||||
gfx.fill(trackX, y + 30, trackX + 2, y + 30 + trackHeight, SkillUi.BORDER);
|
||||
gfx.fill(trackX, thumbY, trackX + 2, thumbY + 16, SkillUi.ACCENT);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
SkillUi.panel(gfx, x, y, inspectorWidth(), inspectorHeight());
|
||||
gfx.fill(x, y, x + inspectorWidth(), y + 25, SkillUi.HEADER);
|
||||
gfx.fill(x + 12, y + 29, x + inspectorWidth() - 12, y + inspectorHeight() - 40, SkillUi.SURFACE);
|
||||
}
|
||||
|
||||
private boolean hasSelection() {
|
||||
return selectedNode != null || selectedAbility != null;
|
||||
}
|
||||
|
||||
private boolean usesSidebar() {
|
||||
return width >= 560;
|
||||
}
|
||||
|
||||
private int inspectorWidth() {
|
||||
return Math.min(OVERLAY_WIDTH, width - 24);
|
||||
}
|
||||
|
||||
private int inspectorHeight() {
|
||||
return Math.min(300, height - 72);
|
||||
}
|
||||
|
||||
private int inspectorX() {
|
||||
return inspectorRestX() + inspectorSlideOffset;
|
||||
}
|
||||
|
||||
private int inspectorRestX() {
|
||||
return usesSidebar() ? width - inspectorWidth() - 12 : (width - inspectorWidth()) / 2;
|
||||
}
|
||||
|
||||
private void updateInspectorAnimation() {
|
||||
if (!hasSelection()) return;
|
||||
float progress = Mth.clamp((Util.getMillis() - inspectorOpenedAt) / INSPECTOR_SLIDE_MS, 0.0f, 1.0f);
|
||||
float remaining = 1.0f - progress;
|
||||
if (inspectorClosing && (progress >= 1.0f || !usesSidebar())) {
|
||||
finishClosingInspector();
|
||||
return;
|
||||
}
|
||||
float eased = 1.0f - remaining * remaining * remaining;
|
||||
int targetOffset = inspectorClosing ? inspectorWidth() + 16 : 0;
|
||||
inspectorSlideOffset = usesSidebar() ? Math.round(inspectorAnimationStartOffset
|
||||
+ (targetOffset - inspectorAnimationStartOffset) * eased) : 0;
|
||||
if (unlockButton != null) unlockButton.setX(inspectorX() + inspectorWidth() - 96);
|
||||
if (inspectorCloseButton != null) inspectorCloseButton.setX(inspectorX() + inspectorWidth() - 23);
|
||||
}
|
||||
|
||||
private int inspectorY() {
|
||||
return usesSidebar() ? 40 : (height - inspectorHeight()) / 2;
|
||||
}
|
||||
|
||||
private boolean isOverInspector(double x, double y) {
|
||||
return x >= inspectorX() - 1 && x <= inspectorX() + inspectorWidth() + 1
|
||||
&& y >= inspectorY() - 1 && y <= inspectorY() + inspectorHeight() + 1;
|
||||
}
|
||||
|
||||
private void revealSelection(float x, float y) {
|
||||
if (!usesSidebar()) return;
|
||||
int[] position = worldToScreen(x, y);
|
||||
// Move only a selection that the actual popup rectangle would cover.
|
||||
// Nodes above or below it keep their existing position.
|
||||
int margin = 24;
|
||||
if (position[0] >= inspectorRestX() - margin
|
||||
&& position[0] <= inspectorRestX() + inspectorWidth() + margin
|
||||
&& position[1] >= inspectorY() - margin
|
||||
&& position[1] <= inspectorY() + inspectorHeight() + margin) {
|
||||
panX += inspectorRestX() - margin - position[0];
|
||||
}
|
||||
}
|
||||
|
||||
private void closeInspector() {
|
||||
if (!hasSelection() || inspectorClosing) return;
|
||||
if (!usesSidebar()) {
|
||||
finishClosingInspector();
|
||||
return;
|
||||
}
|
||||
inspectorClosing = true;
|
||||
inspectorAnimationStartOffset = inspectorSlideOffset;
|
||||
inspectorOpenedAt = Util.getMillis();
|
||||
if (unlockButton != null) {
|
||||
unlockButton.active = false;
|
||||
unlockButton.setTooltip(null);
|
||||
}
|
||||
if (inspectorCloseButton != null) {
|
||||
inspectorCloseButton.active = false;
|
||||
inspectorCloseButton.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishClosingInspector() {
|
||||
selectedNode = null;
|
||||
selectedAbility = null;
|
||||
unlockButton = null;
|
||||
inspectorCloseButton = null;
|
||||
inspectorSlideOffset = 0;
|
||||
inspectorAnimationStartOffset = 0;
|
||||
inspectorClosing = false;
|
||||
clearWidgets();
|
||||
rebuildTreeTabs();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (keyCode == 256 && legendOpen) {
|
||||
legendOpen = false;
|
||||
return true;
|
||||
}
|
||||
if (keyCode == 256 && hasSelection()) {
|
||||
closeInspector();
|
||||
return true;
|
||||
}
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
|
||||
private int renderPrerequisites(GuiGraphics gfx, List<ResourceLocation> links, int x, int y, boolean ability) {
|
||||
if (links.isEmpty()) return y;
|
||||
gfx.drawString(font, "Required Skills:", x + 16, y, SkillUi.ACCENT, false);
|
||||
y += 12;
|
||||
for (ResourceLocation id : links) {
|
||||
SkillNode node = SkillNodeRegistry.get(id);
|
||||
AbilityNode parentAbility = AbilityNodeRegistry.get(id);
|
||||
Component name = node != null ? node.getTitle()
|
||||
: parentAbility != null ? parentAbility.getTitle() : Component.literal(id.toString());
|
||||
boolean met = SkilltreeClientState.isUnlocked(id)
|
||||
|| (ability && SkilltreeClientState.isAbilityUnlocked(id));
|
||||
Component line = Component.literal(met ? "+ " : "- ").append(name);
|
||||
gfx.drawWordWrap(font, line, x + 16, y, inspectorWidth() - 32, met ? UNLOCKED : BLOCKED);
|
||||
y += font.split(line, inspectorWidth() - 32).size() * font.lineHeight + 3;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
private void updateUnlockButton(String status, boolean canUnlock) {
|
||||
if (unlockButton == null || inspectorClosing) return;
|
||||
unlockButton.setTooltip(Tooltip.create(Component.literal(status)));
|
||||
unlockButton.active = canUnlock;
|
||||
}
|
||||
|
||||
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());
|
||||
rebuildInspectorButtons(false);
|
||||
}
|
||||
|
||||
private void rebuildAbilityOverlayButtons() {
|
||||
rebuildInspectorButtons(true);
|
||||
}
|
||||
|
||||
private void rebuildInspectorButtons(boolean ability) {
|
||||
if (unlockButton == null || inspectorClosing) {
|
||||
inspectorAnimationStartOffset = unlockButton == null
|
||||
? (usesSidebar() ? inspectorWidth() + 16 : 0) : inspectorSlideOffset;
|
||||
inspectorOpenedAt = Util.getMillis();
|
||||
inspectorSlideOffset = inspectorAnimationStartOffset;
|
||||
}
|
||||
inspectorClosing = false;
|
||||
detailScroll = 0;
|
||||
maxDetailScroll = 0;
|
||||
dragging = false;
|
||||
clearUnlockHighlights();
|
||||
clearWidgets();
|
||||
if (selectedAbility == null) {
|
||||
return;
|
||||
addLegendButton();
|
||||
int x = inspectorX();
|
||||
int y = inspectorY();
|
||||
unlockButton = addRenderableWidget(SkillUi.button(Component.literal("Unlock"), btn -> {
|
||||
if (inspectorClosing) return;
|
||||
if (ability ? !prepareAbilityUnlock(selectedAbility) : !prepareSkillUnlock(selectedNode)) return;
|
||||
ResourceLocation id = ability ? selectedAbility.getId() : selectedNode.getId();
|
||||
PacketHandler.CHANNEL.sendToServer(new ServerboundUnlockNodePacket(id));
|
||||
closeInspector();
|
||||
}).pos(x + inspectorWidth() - 96, y + inspectorHeight() - 28).size(80, 20).build());
|
||||
unlockButton.active = ability
|
||||
? !SkilltreeClientState.isAbilityUnlocked(selectedAbility.getId()) && abilityRequirementsMet(selectedAbility)
|
||||
: getNodeColor(selectedNode) == AVAILABLE;
|
||||
inspectorCloseButton = addRenderableWidget(SkillUi.button(Component.literal("X"), btn -> closeInspector())
|
||||
.pos(x + inspectorWidth() - 23, y + 5).size(18, 16).build());
|
||||
inspectorCloseButton.setTooltip(Tooltip.create(Component.literal("Close details (Esc)")));
|
||||
}
|
||||
|
||||
private void addLegendButton() {
|
||||
Button help = addRenderableWidget(SkillUi.button(Component.literal("?"), btn -> legendOpen = !legendOpen)
|
||||
.pos(12, height - 21).size(20, 18).build());
|
||||
help.setTooltip(Tooltip.create(Component.literal("Show or hide the legend")));
|
||||
}
|
||||
|
||||
private boolean isOverLegend(double x, double y) {
|
||||
return x >= 12 && x <= 12 + Math.min(300, width - 24)
|
||||
&& y >= height - 158 && y <= height - 30;
|
||||
}
|
||||
|
||||
private void renderLegend(GuiGraphics gfx) {
|
||||
int x = 12;
|
||||
int y = height - 158;
|
||||
SkillUi.panel(gfx, x, y, Math.min(300, width - 24), 128);
|
||||
gfx.drawString(font, "Node borders and paths", x + 12, y + 10, SkillUi.TEXT, false);
|
||||
String[] labels = {"Unlocked", "Available to unlock",
|
||||
"Unavailable: requirements or points missing", "Blocked: mutually exclusive"};
|
||||
int[] colors = {UNLOCKED, AVAILABLE, UNAVAILABLE, BLOCKED};
|
||||
for (int i = 0; i < labels.length; i++) {
|
||||
int rowY = y + 30 + i * 15;
|
||||
gfx.fill(x + 12, rowY, x + 20, rowY + 8, colors[i]);
|
||||
gfx.drawString(font, labels[i], x + 26, rowY, SkillUi.TEXT, false);
|
||||
}
|
||||
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());
|
||||
gfx.drawString(font, "White outline: hover or selection", x + 12, y + 110, SkillUi.MUTED, false);
|
||||
}
|
||||
|
||||
private int[] worldToScreen(float wx, float wy) {
|
||||
@@ -529,37 +784,42 @@ public class SkilltreeScreen extends Screen {
|
||||
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;
|
||||
}
|
||||
private String skillStatus(SkillNode node) {
|
||||
if (SkilltreeClientState.isUnlocked(node.getId())) return "Already unlocked";
|
||||
for (ResourceLocation id : node.getExclusiveWith()) {
|
||||
if (!SkilltreeClientState.isUnlocked(id)) continue;
|
||||
SkillNode blocker = SkillNodeRegistry.get(id);
|
||||
return "Blocked by: " + (blocker == null ? id.toString() : blocker.getTitle().getString());
|
||||
}
|
||||
return color;
|
||||
if (SkilltreeClientState.getCurrentSkillPoints() < Math.max(0, node.getCost())) return "Not enough skill points";
|
||||
if (!node.getLinks().stream().allMatch(SkilltreeClientState::isUnlocked)) return "Unlock prerequisites first";
|
||||
if (!meetsClientAttributes(node)) return "Attribute requirements not met";
|
||||
return "Ready to unlock";
|
||||
}
|
||||
|
||||
private int getNodeColor(SkillNode node) {
|
||||
if (SkilltreeClientState.isUnlocked(node.getId())) return UNLOCKED;
|
||||
if (isLockedByExclusivity(node)) return BLOCKED;
|
||||
return node.getLinks().stream().allMatch(SkilltreeClientState::isUnlocked)
|
||||
&& meetsClientAttributes(node)
|
||||
&& SkilltreeClientState.getCurrentSkillPoints() >= Math.max(0, node.getCost())
|
||||
? AVAILABLE : UNAVAILABLE;
|
||||
}
|
||||
|
||||
private boolean abilityRequirementsMet(AbilityNode ability) {
|
||||
return ability.getLinks().stream().allMatch(id -> SkilltreeClientState.isUnlocked(id)
|
||||
|| SkilltreeClientState.isAbilityUnlocked(id));
|
||||
}
|
||||
|
||||
private int abilityPathColor(AbilityNode ability, ResourceLocation parent) {
|
||||
boolean parentUnlocked = SkilltreeClientState.isUnlocked(parent) || SkilltreeClientState.isAbilityUnlocked(parent);
|
||||
if (parentUnlocked && SkilltreeClientState.isAbilityUnlocked(ability.getId())) return UNLOCKED;
|
||||
return parentUnlocked && abilityRequirementsMet(ability) ? AVAILABLE : LOCKED_PATH;
|
||||
}
|
||||
|
||||
private boolean prepareSkillUnlock(SkillNode target) {
|
||||
clearUnlockHighlights();
|
||||
if (SkilltreeClientState.isUnlocked(target.getId()) || isLockedByExclusivity(target)) return false;
|
||||
for (ResourceLocation parentId : target.getLinks()) {
|
||||
if (SkilltreeClientState.isUnlocked(parentId)) continue;
|
||||
SkillNode parent = SkillNodeRegistry.get(parentId);
|
||||
@@ -598,6 +858,7 @@ public class SkilltreeScreen extends Screen {
|
||||
|
||||
private boolean prepareAbilityUnlock(AbilityNode ability) {
|
||||
clearUnlockHighlights();
|
||||
if (SkilltreeClientState.isAbilityUnlocked(ability.getId())) return false;
|
||||
for (ResourceLocation parentId : ability.getLinks()) {
|
||||
if (SkilltreeClientState.isUnlocked(parentId)
|
||||
|| SkilltreeClientState.isAbilityUnlocked(parentId)) continue;
|
||||
@@ -632,7 +893,7 @@ public class SkilltreeScreen extends Screen {
|
||||
if (unlockFailureMessage == null) return;
|
||||
int messageWidth = Math.min(width - 40, font.width(unlockFailureMessage) + 20);
|
||||
int x = (width - messageWidth) / 2;
|
||||
int y = height - 34;
|
||||
int y = height - 46;
|
||||
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);
|
||||
@@ -667,16 +928,21 @@ public class SkilltreeScreen extends Screen {
|
||||
}
|
||||
|
||||
private void rebuildTreeTabs() {
|
||||
addLegendButton();
|
||||
List<ResourceLocation> trees = SkillNodeRegistry.trees();
|
||||
if (trees.size() <= 1 || selectedNode != null || selectedAbility != null) {
|
||||
return;
|
||||
}
|
||||
int x = 10;
|
||||
int y = 28;
|
||||
int x = 14;
|
||||
int y = 44;
|
||||
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 -> {
|
||||
if (x + buttonWidth > width - 14 && x > 14) {
|
||||
x = 14;
|
||||
y += 26;
|
||||
}
|
||||
Button button = addRenderableWidget(SkillUi.button(Component.literal(label), btn -> {
|
||||
activeTree = tree;
|
||||
selectedNode = null;
|
||||
selectedAbility = null;
|
||||
@@ -687,7 +953,7 @@ public class SkilltreeScreen extends Screen {
|
||||
.size(buttonWidth, 20)
|
||||
.build());
|
||||
button.active = !tree.equals(activeTree);
|
||||
x += buttonWidth + 4;
|
||||
x += buttonWidth + 6;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user