extend ability system to support dynamic slot counts, add skill tree validation, new prerequisites API, and extended mod commands

This commit is contained in:
2026-05-25 12:33:49 +02:00
parent f7332054ea
commit 77fbada438
32 changed files with 961 additions and 128 deletions
@@ -35,27 +35,23 @@ public class AbilityBindingScreen extends Screen {
protected void init() {
int x = (width - PANEL_WIDTH) / 2;
int y = (height - PANEL_HEIGHT) / 2;
int buttonRowWidth = SLOT_BUTTON_WIDTH * 3 + BUTTON_GAP * 2;
int slotCount = SkilltreeClientState.getAbilitySlots().length;
int visibleSlots = Math.min(slotCount, 3);
int buttonRowWidth = SLOT_BUTTON_WIDTH * visibleSlots + BUTTON_GAP * Math.max(0, visibleSlots - 1);
int buttonX = x + (PANEL_WIDTH - buttonRowWidth) / 2;
int buttonY = y + PANEL_HEIGHT - 54;
int boundSlot = SkilltreeClientState.getSlotOfAbility(ability.getId());
Button yBtn = addRenderableWidget(Button.builder(Component.literal("Slot 1"),
b -> bind(1)).pos(buttonX, buttonY).size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT).build());
if (boundSlot == 1) {
yBtn.active = false;
yBtn.setMessage(Component.literal("Bound 1"));
}
Button xBtn = addRenderableWidget(Button.builder(Component.literal("Slot 2"),
b -> bind(2)).pos(buttonX + SLOT_BUTTON_WIDTH + BUTTON_GAP, buttonY).size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT).build());
if (boundSlot == 2) {
xBtn.active = false;
xBtn.setMessage(Component.literal("Bound 2"));
}
Button cBtn = addRenderableWidget(Button.builder(Component.literal("Slot 3"),
b -> bind(3)).pos(buttonX + (SLOT_BUTTON_WIDTH + BUTTON_GAP) * 2, buttonY).size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT).build());
if (boundSlot == 3) {
cBtn.active = false;
cBtn.setMessage(Component.literal("Bound 3"));
for (int slot = 1; slot <= visibleSlots; slot++) {
int currentSlot = slot;
Button button = addRenderableWidget(Button.builder(Component.literal("Slot " + slot),
b -> bind(currentSlot))
.pos(buttonX + (SLOT_BUTTON_WIDTH + BUTTON_GAP) * (slot - 1), buttonY)
.size(SLOT_BUTTON_WIDTH, BUTTON_HEIGHT)
.build());
if (boundSlot == slot) {
button.active = false;
button.setMessage(Component.literal("Bound " + slot));
}
}
addRenderableWidget(Button.builder(Component.literal("Cancel"),
b -> onClose()).pos(x + PANEL_WIDTH / 2 - 40, y + PANEL_HEIGHT - 28).size(80, BUTTON_HEIGHT).build());
@@ -13,7 +13,8 @@ public class AbilityHudOverlay {
if (mc.player == null) return;
int xStart = event.getGuiGraphics().guiWidth() - 80;
int yStart = 10;
for (int slot = 1; slot <= 3; slot++) {
int slots = SkilltreeClientState.getAbilitySlots().length;
for (int slot = 1; slot <= slots; slot++) {
var abilityId = SkilltreeClientState.getAbilityInSlot(slot);
if (abilityId == null) continue;
AbilityNode ability = AbilityNodeRegistry.get(abilityId);
@@ -4,43 +4,45 @@ import com.mojang.blaze3d.platform.InputConstants;
import net.fretux.skillengine.network.PacketHandler;
import net.fretux.skillengine.network.ServerboundActivateAbilityPacket;
import net.minecraft.client.KeyMapping;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.client.settings.KeyConflictContext;
import org.lwjgl.glfw.GLFW;
public class AbilityKeybindHandler {
public static final KeyMapping ABILITY_1 = new KeyMapping(
"key.skillengine.ability1", KeyConflictContext.IN_GAME,
InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_Y, "key.categories.gameplay"
);
public static final int MAX_REGISTERED_ABILITY_SLOTS = 9;
public static final KeyMapping[] ABILITY_KEYS = new KeyMapping[MAX_REGISTERED_ABILITY_SLOTS];
public static final KeyMapping ABILITY_2 = new KeyMapping(
"key.skillengine.ability2", KeyConflictContext.IN_GAME,
InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_X, "key.categories.gameplay"
);
public static final KeyMapping ABILITY_3 = new KeyMapping(
"key.skillengine.ability3", KeyConflictContext.IN_GAME,
InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_C, "key.categories.gameplay"
);
static {
for (int slot = 1; slot <= MAX_REGISTERED_ABILITY_SLOTS; slot++) {
ABILITY_KEYS[slot - 1] = new KeyMapping(
"key.skillengine.ability" + slot,
KeyConflictContext.IN_GAME,
InputConstants.Type.KEYSYM,
defaultKeyForSlot(slot),
"key.categories.gameplay"
);
}
}
public static void onInput(InputEvent.Key event) {
if (ABILITY_1.consumeClick()) {
if (SkilltreeClientState.getClientCooldown(1) <= 0) {
PacketHandler.CHANNEL.sendToServer(new ServerboundActivateAbilityPacket(1));
}
}
if (ABILITY_2.consumeClick()) {
if (SkilltreeClientState.getClientCooldown(2) <= 0) {
PacketHandler.CHANNEL.sendToServer(new ServerboundActivateAbilityPacket(2));
}
}
if (ABILITY_3.consumeClick()) {
if (SkilltreeClientState.getClientCooldown(3) <= 0) {
PacketHandler.CHANNEL.sendToServer(new ServerboundActivateAbilityPacket(3));
int activeSlots = SkilltreeClientState.getAbilitySlots().length;
int handledSlots = Math.min(activeSlots, ABILITY_KEYS.length);
for (int slot = 1; slot <= handledSlots; slot++) {
if (ABILITY_KEYS[slot - 1].consumeClick()) {
if (SkilltreeClientState.getClientCooldown(slot) <= 0) {
PacketHandler.CHANNEL.sendToServer(new ServerboundActivateAbilityPacket(slot));
}
}
}
}
private static int defaultKeyForSlot(int slot) {
return switch (slot) {
case 1 -> GLFW.GLFW_KEY_Z;
case 2 -> GLFW.GLFW_KEY_X;
case 3 -> GLFW.GLFW_KEY_C;
default -> InputConstants.UNKNOWN.getValue();
};
}
}
@@ -12,7 +12,7 @@ import org.jetbrains.annotations.NotNull;
public class AbilitySlotsScreen extends Screen {
private static final int PANEL_WIDTH = 260;
private static final int PANEL_HEIGHT = 180;
private static final int MIN_PANEL_HEIGHT = 180;
private static final int SLOT_TEXT_RIGHT_PADDING = 8;
private static final int REBIND_BUTTON_WIDTH = 80;
public AbilitySlotsScreen() {
@@ -22,21 +22,19 @@ public class AbilitySlotsScreen extends Screen {
@Override
protected void init() {
int x = (width - PANEL_WIDTH) / 2;
int y = (height - PANEL_HEIGHT) / 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;
// Y slot = 1
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(1))
.pos(buttonX, rowY).size(REBIND_BUTTON_WIDTH, buttonH).build());
// X slot = 2
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(2))
.pos(buttonX, rowY + 35).size(REBIND_BUTTON_WIDTH, buttonH).build());
// C slot = 3
addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(3))
.pos(buttonX, rowY + 70).size(REBIND_BUTTON_WIDTH, buttonH).build());
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(Button.builder(Component.literal("Close"), b -> onClose())
.pos(x + PANEL_WIDTH - 70, y + PANEL_HEIGHT - 28).size(60, 20).build());
.pos(x + PANEL_WIDTH - 70, y + panelHeight - 28).size(60, 20).build());
}
private void openSelect(int slot) {
@@ -57,14 +55,16 @@ public class AbilitySlotsScreen 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 panelHeight = panelHeight();
int y = (height - panelHeight) / 2;
gfx.fill(0, 0, width, height, 0xAA000000);
gfx.fill(x, y, x + PANEL_WIDTH, y + PANEL_HEIGHT, 0xFF222222);
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;
drawSlotRow(gfx, x + 15, rowY, 1, "Y");
drawSlotRow(gfx, x + 15, rowY + 35, 2, "X");
drawSlotRow(gfx, x + 15, rowY + 70, 3, "C");
int slots = SkilltreeClientState.getAbilitySlots().length;
for (int slot = 1; slot <= slots; slot++) {
drawSlotRow(gfx, x + 15, rowY + (slot - 1) * 35, slot, "Slot " + slot);
}
super.render(gfx, mouseX, mouseY, partialTick);
}
@@ -106,4 +106,9 @@ public class AbilitySlotsScreen extends Screen {
}
return font.plainSubstrByWidth(title, availableWidth - suffixWidth) + suffix;
}
private int panelHeight() {
int slots = SkilltreeClientState.getAbilitySlots().length;
return Math.max(MIN_PANEL_HEIGHT, 80 + slots * 35);
}
}
@@ -5,10 +5,11 @@ import net.minecraftforge.event.TickEvent;
public class ClientCooldownTicker {
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) return;
for (int i = 0; i < 3; i++) {
int cd = SkilltreeClientState.getClientCooldown(i + 1);
int slots = SkilltreeClientState.getAbilitySlots().length;
for (int slot = 1; slot <= slots; slot++) {
int cd = SkilltreeClientState.getClientCooldown(slot);
if (cd > 0) {
SkilltreeClientState.updateCooldown(i + 1, cd - 1);
SkilltreeClientState.updateCooldown(slot, cd - 1);
}
}
}
@@ -24,8 +24,8 @@ public class ClientKeybinds {
);
event.register(OPEN_ABILITIES);
event.register(AbilityKeybindHandler.ABILITY_1);
event.register(AbilityKeybindHandler.ABILITY_2);
event.register(AbilityKeybindHandler.ABILITY_3);
for (KeyMapping keyMapping : AbilityKeybindHandler.ABILITY_KEYS) {
event.register(keyMapping);
}
}
}
@@ -14,16 +14,18 @@ public class SkilltreeClientState {
private static final Set<ResourceLocation> unlocked = new HashSet<>();
private static final Set<ResourceLocation> unlockedAbilities = new HashSet<>();
private static int currentSkillPoints = 0;
private static final ResourceLocation[] abilitySlots = new ResourceLocation[3];
private static final int[] clientCooldowns = new int[3];
private static ResourceLocation[] abilitySlots = new ResourceLocation[3];
private static int[] clientCooldowns = new int[3];
public static void updateCooldown(int slot, int cd) {
ensureSlot(slot);
int index = slot - 1;
if (index < 0 || index >= clientCooldowns.length) return;
clientCooldowns[index] = Math.max(0, cd);
}
public static int getClientCooldown(int slot) {
ensureSlot(slot);
int index = slot - 1;
if (index < 0 || index >= clientCooldowns.length) return 0;
return clientCooldowns[index];
@@ -74,16 +76,20 @@ public class SkilltreeClientState {
}
public static void bindAbilityLocal(int slot, ResourceLocation abilityId) {
ensureSlot(slot);
PlayerSkillData.bindHelper(slot, abilityId, abilitySlots);
}
public static void setAbilitySlots(ResourceLocation[] slots) {
abilitySlots = new ResourceLocation[slots != null ? slots.length : 3];
clientCooldowns = Arrays.copyOf(clientCooldowns, abilitySlots.length);
for (int i = 0; i < abilitySlots.length; i++) {
abilitySlots[i] = (slots != null && i < slots.length) ? slots[i] : null;
}
}
public static ResourceLocation getAbilityInSlot(int slot) {
ensureSlot(slot);
int index = slot - 1;
if (index < 0 || index >= abilitySlots.length) return null;
return abilitySlots[index];
@@ -100,4 +106,10 @@ public class SkilltreeClientState {
public static ResourceLocation[] getAbilitySlots() {
return Arrays.copyOf(abilitySlots, abilitySlots.length);
}
private static void ensureSlot(int slot) {
if (slot <= abilitySlots.length) return;
abilitySlots = Arrays.copyOf(abilitySlots, slot);
clientCooldowns = Arrays.copyOf(clientCooldowns, slot);
}
}
@@ -38,6 +38,7 @@ public class SkilltreeScreen extends Screen {
private SkillNode selectedNode = null;
private AbilityNode hoveredAbility = null;
private AbilityNode selectedAbility = null;
private ResourceLocation activeTree = null;
public SkilltreeScreen() {
super(Component.literal("Ascend Skilltree"));
@@ -46,6 +47,11 @@ public class SkilltreeScreen extends Screen {
@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
@@ -141,7 +147,7 @@ public class SkilltreeScreen extends Screen {
}
private void drawGraph(GuiGraphics gfx) {
for (SkillNode node : SkillNodeRegistry.all()) {
for (SkillNode node : activeSkillNodes()) {
int[] p1 = worldToScreen(node.getX(), node.getY());
for (ResourceLocation neighborId : node.getLinks()) {
SkillNode neighbor = SkillNodeRegistry.get(neighborId);
@@ -193,7 +199,7 @@ public class SkilltreeScreen extends Screen {
gfx.blit(icon, pos[0] - 8, pos[1] - 8, 0, 0, 16, 16, 16, 16);
}
}
for (SkillNode node : SkillNodeRegistry.all()) {
for (SkillNode node : activeSkillNodes()) {
int[] pos = worldToScreen(node.getX(), node.getY());
int color = getNodeColor(node);
gfx.fill(
@@ -364,6 +370,7 @@ public class SkilltreeScreen extends Screen {
);
selectedNode = null;
clearWidgets();
rebuildTreeTabs();
}).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20)
.size(80, 20)
.build());
@@ -371,6 +378,7 @@ public class SkilltreeScreen extends Screen {
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
selectedNode = null;
clearWidgets();
rebuildTreeTabs();
}).pos(x + OVERLAY_WIDTH - 60, y - 5)
.size(50, 20)
.build());
@@ -391,6 +399,7 @@ public class SkilltreeScreen extends Screen {
);
selectedAbility = null;
clearWidgets();
rebuildTreeTabs();
}).pos(x + OVERLAY_WIDTH / 2 - 40, y + OVERLAY_CONTENT_HEIGHT - 20)
.size(80, 20)
.build());
@@ -398,6 +407,7 @@ public class SkilltreeScreen extends Screen {
addRenderableWidget(Button.builder(Component.literal("Close"), btn -> {
selectedAbility = null;
clearWidgets();
rebuildTreeTabs();
}).pos(x + OVERLAY_WIDTH - 60, y - 5)
.size(50, 20)
.build());
@@ -410,7 +420,7 @@ public class SkilltreeScreen extends Screen {
}
private SkillNode findNodeAt(double mouseX, double mouseY) {
for (SkillNode node : SkillNodeRegistry.all()) {
for (SkillNode node : activeSkillNodes()) {
int[] pos = worldToScreen(node.getX(), node.getY());
if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], NODE_RADIUS)) {
return node;
@@ -483,8 +493,37 @@ public class SkilltreeScreen extends Screen {
gfx.pose().popPose();
}
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;
}
}
}