699 lines
30 KiB
Java
699 lines
30 KiB
Java
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<ResourceLocation> 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<Component> tooltip = new ArrayList<>();
|
|
tooltip.add(node.getTitle());
|
|
String desc = node.getDescription().getString();
|
|
List<FormattedText> 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<SkillNode> activeSkillNodes() {
|
|
return activeTree != null ? SkillNodeRegistry.byTree(activeTree) : SkillNodeRegistry.all();
|
|
}
|
|
|
|
private void rebuildTreeTabs() {
|
|
List<ResourceLocation> 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;
|
|
}
|
|
}
|