From 96ea5977e650d4bd536add0f2994a14ed78dcb80 Mon Sep 17 00:00:00 2001 From: fretuks Date: Fri, 5 Dec 2025 14:02:57 +0100 Subject: [PATCH] Implement mutual exclusives --- .../capability/PlayerSkillData.java | 82 +++- .../client/AbilityBindingScreen.java | 94 ++++ .../client/AbilitySelectScreen.java | 120 +++++ .../client/AbilitySlotsScreen.java | 98 +++++ .../skillengine/client/ClientEvents.java | 4 + .../skillengine/client/ClientKeybinds.java | 8 + .../client/SkilltreeClientState.java | 50 ++- .../skillengine/client/SkilltreeScreen.java | 415 ++++++++++++------ .../events/SkillEnginePlayerEvents.java | 7 +- .../ClientboundNodeUnlockedPacket.java | 7 +- .../network/ClientboundSyncSkillsPacket.java | 46 +- .../skillengine/network/PacketHandler.java | 12 +- .../network/ServerboundBindAbilityPacket.java | 44 ++ .../network/ServerboundUnlockNodePacket.java | 50 ++- .../skillengine/skilltree/AbilityNode.java | 48 ++ .../skilltree/AbilityNodeLoader.java | 71 +++ .../skilltree/AbilityNodeRegistry.java | 28 ++ .../skillengine/abilitynodes/berserk.json | 15 + .../data/skillengine/skillnodes/strength.json | 17 + 19 files changed, 1040 insertions(+), 176 deletions(-) create mode 100644 src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java create mode 100644 src/main/java/net/fretux/skillengine/client/AbilitySelectScreen.java create mode 100644 src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java create mode 100644 src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java create mode 100644 src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java create mode 100644 src/main/java/net/fretux/skillengine/skilltree/AbilityNodeLoader.java create mode 100644 src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java create mode 100644 src/main/resources/data/skillengine/abilitynodes/berserk.json create mode 100644 src/main/resources/data/skillengine/skillnodes/strength.json diff --git a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java index 005e8af..8a48397 100644 --- a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java +++ b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java @@ -1,5 +1,6 @@ package net.fretux.skillengine.capability; +import net.fretux.skillengine.skilltree.AbilityNode; import net.fretux.skillengine.skilltree.SkillNode; import net.fretux.skillengine.skilltree.SkillNodeRegistry; import net.minecraft.nbt.CompoundTag; @@ -14,22 +15,22 @@ public class PlayerSkillData { private int skillPoints; private final Set unlockedNodes = new HashSet<>(); + private final Set unlockedAbilities = new HashSet<>(); private final Set activeTags = new HashSet<>(); public int getSkillPoints() { return skillPoints; } public void addSkillPoints(int amount) { skillPoints += amount; } - public boolean isUnlocked(ResourceLocation nodeId) { return unlockedNodes.contains(nodeId); } - public boolean hasTag(ResourceLocation tag) { return activeTags.contains(tag); } - public Set getActiveTags() { return activeTags; } + private final ResourceLocation[] abilitySlots = new ResourceLocation[3]; + public void unlockNode(SkillNode node) { if (unlockedNodes.add(node.getId())) { @@ -41,7 +42,6 @@ public class PlayerSkillData { public CompoundTag save() { CompoundTag tag = new CompoundTag(); tag.putInt("SkillPoints", skillPoints); - ListTag unlockedList = new ListTag(); for (ResourceLocation id : unlockedNodes) { CompoundTag t = new CompoundTag(); @@ -49,7 +49,6 @@ public class PlayerSkillData { unlockedList.add(t); } tag.put("Unlocked", unlockedList); - ListTag tagList = new ListTag(); for (ResourceLocation id : activeTags) { CompoundTag t = new CompoundTag(); @@ -57,7 +56,24 @@ public class PlayerSkillData { tagList.add(t); } tag.put("Tags", tagList); - + ListTag abilityList = new ListTag(); + for (int i = 0; i < abilitySlots.length; i++) { + CompoundTag t = new CompoundTag(); + if (abilitySlots[i] != null) { + t.putString("Id", abilitySlots[i].toString()); + } else { + t.putString("Id", ""); + } + abilityList.add(t); + } + tag.put("AbilitySlots", abilityList); + ListTag unlockedAbilitiesList = new ListTag(); + for (ResourceLocation id : unlockedAbilities) { + CompoundTag t = new CompoundTag(); + t.putString("Id", id.toString()); + unlockedAbilitiesList.add(t); + } + tag.put("UnlockedAbilities", unlockedAbilitiesList); return tag; } @@ -65,18 +81,32 @@ public class PlayerSkillData { skillPoints = tag.getInt("SkillPoints"); unlockedNodes.clear(); activeTags.clear(); - + unlockedAbilities.clear(); ListTag unlockedList = tag.getList("Unlocked", Tag.TAG_COMPOUND); for (Tag t : unlockedList) { CompoundTag c = (CompoundTag) t; unlockedNodes.add(new ResourceLocation(c.getString("Id"))); } - ListTag tagList = tag.getList("Tags", Tag.TAG_COMPOUND); for (Tag t : tagList) { CompoundTag c = (CompoundTag) t; activeTags.add(new ResourceLocation(c.getString("Tag"))); } + ListTag abilityList = tag.getList("AbilitySlots", Tag.TAG_COMPOUND); + for (int i = 0; i < abilitySlots.length && i < abilityList.size(); i++) { + CompoundTag t = abilityList.getCompound(i); + String id = t.getString("Id"); + if (!id.isEmpty()) { + abilitySlots[i] = new ResourceLocation(id); + } else { + abilitySlots[i] = null; + } + } + ListTag unlockedAbilitiesList = tag.getList("UnlockedAbilities", Tag.TAG_COMPOUND); + for (Tag t : unlockedAbilitiesList) { + CompoundTag c = (CompoundTag) t; + unlockedAbilities.add(new ResourceLocation(c.getString("Id"))); + } } public Set getUnlockedNodes() { @@ -98,4 +128,40 @@ public class PlayerSkillData { unlockedNodes.clear(); activeTags.clear(); } + + public void bindAbility(int slot, ResourceLocation abilityId) { + int index = slot - 1; + if (index < 0 || index >= abilitySlots.length) return; + // Ensure the same ability is not present in multiple slots + if (abilityId != null) { + for (int i = 0; i < abilitySlots.length; i++) { + if (abilityId.equals(abilitySlots[i])) { + abilitySlots[i] = null; + } + } + } + abilitySlots[index] = abilityId; + } + + public ResourceLocation getAbilityInSlot(int slot) { + int index = slot - 1; + if (index < 0 || index >= abilitySlots.length) return null; + return abilitySlots[index]; + } + + public ResourceLocation[] getAbilitySlots() { + return abilitySlots.clone(); + } + + public boolean isAbilityUnlocked(ResourceLocation id) { + return unlockedAbilities.contains(id); + } + + public void unlockAbility(AbilityNode node) { + unlockedAbilities.add(node.getId()); + } + + public Set getUnlockedAbilities() { + return unlockedAbilities; + } } \ No newline at end of file diff --git a/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java b/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java new file mode 100644 index 0000000..471ff7e --- /dev/null +++ b/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java @@ -0,0 +1,94 @@ +package net.fretux.skillengine.client; + +import net.fretux.skillengine.SkillEngine; +import net.fretux.skillengine.network.PacketHandler; +import net.fretux.skillengine.network.ServerboundBindAbilityPacket; +import net.fretux.skillengine.skilltree.AbilityNode; +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; + +public class AbilityBindingScreen extends Screen { + + private static final int PANEL_WIDTH = 220; + private static final int PANEL_HEIGHT = 160; + + private final AbilityNode ability; + + public AbilityBindingScreen(AbilityNode ability) { + super(Component.literal("Bind Ability")); + this.ability = ability; + } + + @Override + protected void init() { + int x = (width - PANEL_WIDTH) / 2; + int y = (height - PANEL_HEIGHT) / 2; + int buttonX = x + 20; + int buttonY = y + 80; + int boundSlot = SkilltreeClientState.getSlotOfAbility(ability.getId()); + Button yBtn = addRenderableWidget(Button.builder(Component.literal("Bind to Y"), + b -> bind(1)).pos(buttonX, buttonY).size(80, 20).build()); + if (boundSlot == 1) { + yBtn.active = false; + yBtn.setMessage(Component.literal("Bound to Y")); + } + Button xBtn = addRenderableWidget(Button.builder(Component.literal("Bind to X"), + b -> bind(2)).pos(buttonX + 90, buttonY).size(80, 20).build()); + if (boundSlot == 2) { + xBtn.active = false; + xBtn.setMessage(Component.literal("Bound to X")); + } + Button cBtn = addRenderableWidget(Button.builder(Component.literal("Bind to C"), + b -> bind(3)).pos(buttonX, buttonY + 30).size(80, 20).build()); + if (boundSlot == 3) { + cBtn.active = false; + cBtn.setMessage(Component.literal("Bound to C")); + } + addRenderableWidget(Button.builder(Component.literal("Cancel"), + b -> onClose()).pos(buttonX + 90, buttonY + 30).size(80, 20).build()); + } + + private void bind(int slot) { + SkilltreeClientState.bindAbilityLocal(slot, ability.getId()); + SkillEngine.LOGGER.debug("[CLIENT] Binding ability {} to slot {} (optimistic)", ability.getId(), slot); + PacketHandler.CHANNEL.sendToServer(new ServerboundBindAbilityPacket(ability.getId(), slot)); + onClose(); + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(null); + } + + @Override + public boolean keyPressed(int key, int scancode, int mods) { + if (key == 256) { + onClose(); + return true; + } + return super.keyPressed(key, scancode, mods); + } + + @Override + public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) { + this.renderBackground(gfx); + 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); + ResourceLocation icon = ability.getIcon(); + if (icon != null) { + gfx.blit(icon, x + PANEL_WIDTH / 2 - 10, y + 10, 0, 0, 20, 20, 20, 20); + } + gfx.drawCenteredString(font, ability.getTitle(), x + PANEL_WIDTH / 2, y + 35, 0xFFFFFF); + if (ability.getDescription() != null) { + gfx.drawWordWrap(font, ability.getDescription(), x + 10, y + 50, PANEL_WIDTH - 20, 0xDDDDDD); + } + + super.render(gfx, mouseX, mouseY, partialTick); + } +} \ No newline at end of file diff --git a/src/main/java/net/fretux/skillengine/client/AbilitySelectScreen.java b/src/main/java/net/fretux/skillengine/client/AbilitySelectScreen.java new file mode 100644 index 0000000..a030d11 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/client/AbilitySelectScreen.java @@ -0,0 +1,120 @@ +package net.fretux.skillengine.client; + +import net.fretux.skillengine.network.PacketHandler; +import net.fretux.skillengine.network.ServerboundBindAbilityPacket; +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 java.util.ArrayList; +import java.util.List; + +public class AbilitySelectScreen extends Screen { + + private final Screen parent; + private final int slot; // 1..3 + + private final List 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; + + public AbilitySelectScreen(Screen parent, int slot) { + super(Component.literal("Select Ability")); + this.parent = parent; + this.slot = slot; + } + + @Override + protected void init() { + unlockedAbilities.clear(); + for (AbilityNode node : AbilityNodeRegistry.all()) { + if (SkilltreeClientState.isAbilityUnlocked(node.getId())) { + unlockedAbilities.add(node); + } + } + buildPageButtons(); + } + + private void buildPageButtons() { + clearWidgets(); + int x = (width - PANEL_WIDTH) / 2; + int y = (height - PANEL_HEIGHT) / 2; + + int start = page * PAGE_SIZE; + int end = Math.min(unlockedAbilities.size(), start + PAGE_SIZE); + int rowY = y + 40; + for (int i = start; i < end; i++) { + AbilityNode node = unlockedAbilities.get(i); + String label = node.getTitle().getString(); + // Disable if already in this slot to avoid redundant bind + 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) + .build(); + if (alreadyInThisSlot) { + btn.active = false; + btn.setMessage(Component.literal(label + " (bound)")); + } + addRenderableWidget(btn); + rowY += 24; + } + // paging controls + if (page > 0) { + addRenderableWidget(Button.builder(Component.literal("< Prev"), b -> { + page--; + buildPageButtons(); + }).pos(x + 15, y + PANEL_HEIGHT - 28).size(60, 20).build()); + } + if ((page + 1) * PAGE_SIZE < unlockedAbilities.size()) { + addRenderableWidget(Button.builder(Component.literal("Next >"), b -> { + page++; + buildPageButtons(); + }).pos(x + 80, y + PANEL_HEIGHT - 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()); + } + + private void select(AbilityNode node) { + ResourceLocation id = node.getId(); + // optimistic client update, then send to server + SkilltreeClientState.bindAbilityLocal(slot, id); + PacketHandler.CHANNEL.sendToServer(new ServerboundBindAbilityPacket(id, slot)); + back(); + } + + private void back() { + Minecraft.getInstance().setScreen(parent); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + back(); + } + + @Override + public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) { + this.renderBackground(gfx); + 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); + 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); + super.render(gfx, mouseX, mouseY, partialTick); + } +} diff --git a/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java b/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java new file mode 100644 index 0000000..fdde696 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java @@ -0,0 +1,98 @@ +package net.fretux.skillengine.client; + +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; + +public class AbilitySlotsScreen extends Screen { + + private static final int PANEL_WIDTH = 260; + private static final int PANEL_HEIGHT = 180; + + public AbilitySlotsScreen() { + super(Component.literal("Abilities")); + } + + @Override + protected void init() { + int x = (width - PANEL_WIDTH) / 2; + int y = (height - PANEL_HEIGHT) / 2; + + int rowY = y + 40; + int buttonW = 80; + int buttonH = 20; + int buttonX = x + PANEL_WIDTH - buttonW - 15; + + // Y slot = 1 + addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(1)) + .pos(buttonX, rowY).size(buttonW, buttonH).build()); + // X slot = 2 + addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(2)) + .pos(buttonX, rowY + 35).size(buttonW, buttonH).build()); + // C slot = 3 + addRenderableWidget(Button.builder(Component.literal("Rebind"), b -> openSelect(3)) + .pos(buttonX, rowY + 70).size(buttonW, buttonH).build()); + + addRenderableWidget(Button.builder(Component.literal("Close"), b -> onClose()) + .pos(x + PANEL_WIDTH - 70, y + PANEL_HEIGHT - 28).size(60, 20).build()); + } + + private void openSelect(int slot) { + Minecraft.getInstance().setScreen(new AbilitySelectScreen(this, slot)); + } + + @Override + public boolean isPauseScreen() { + return false; + } + + @Override + public void onClose() { + Minecraft.getInstance().setScreen(null); + } + + @Override + public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTick) { + this.renderBackground(gfx); + 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); + 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"); + + super.render(gfx, mouseX, mouseY, partialTick); + } + + private void drawSlotRow(GuiGraphics gfx, int x, int y, int slot, String label) { + gfx.drawString(font, label + ":", x, y + 6, 0xFFFFFF); + ResourceLocation abilityId = SkilltreeClientState.getAbilityInSlot(slot); + String title = "Empty"; + ResourceLocation icon = null; + if (abilityId != null) { + AbilityNode node = AbilityNodeRegistry.get(abilityId); + if (node != null) { + title = node.getTitle().getString(); + icon = node.getIcon(); + } else { + title = abilityId.toString(); + } + } + if (icon != null) { + gfx.blit(icon, x + 20, y - 2, 0, 0, 20, 20, 20, 20); + gfx.drawString(font, title, x + 46, y + 6, 0xDDDDDD); + } else { + gfx.drawString(font, title, x + 20, y + 6, 0x888888); + } + } +} diff --git a/src/main/java/net/fretux/skillengine/client/ClientEvents.java b/src/main/java/net/fretux/skillengine/client/ClientEvents.java index 9528d12..32409f4 100644 --- a/src/main/java/net/fretux/skillengine/client/ClientEvents.java +++ b/src/main/java/net/fretux/skillengine/client/ClientEvents.java @@ -19,5 +19,9 @@ public class ClientEvents { Minecraft.getInstance().setScreen(new SkilltreeScreen()); } + if (ClientKeybinds.OPEN_ABILITIES != null && + ClientKeybinds.OPEN_ABILITIES.consumeClick()) { + Minecraft.getInstance().setScreen(new AbilitySlotsScreen()); + } } } \ No newline at end of file diff --git a/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java b/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java index c8ff6f3..868a641 100644 --- a/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java +++ b/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java @@ -15,6 +15,7 @@ import org.lwjgl.glfw.GLFW; ) public class ClientKeybinds { public static KeyMapping OPEN_SKILLTREE; + public static KeyMapping OPEN_ABILITIES; @SubscribeEvent public static void registerKeys(RegisterKeyMappingsEvent event) { @@ -24,5 +25,12 @@ public class ClientKeybinds { "key.categories.ui" ); event.register(OPEN_SKILLTREE); + + OPEN_ABILITIES = new KeyMapping( + "key.skillengine.openabilities", + GLFW.GLFW_KEY_M, + "key.categories.ui" + ); + event.register(OPEN_ABILITIES); } } diff --git a/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java b/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java index eb03466..e444aec 100644 --- a/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java +++ b/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java @@ -12,18 +12,17 @@ import java.util.Set; public class SkilltreeClientState { private static final Set unlocked = new HashSet<>(); + private static final Set unlockedAbilities = new HashSet<>(); private static int currentSkillPoints = 0; + private static final ResourceLocation[] abilitySlots = new ResourceLocation[3]; public static void unlockNode(ResourceLocation id) { SkillEngine.LOGGER.info("[CLIENT] Unlocking node: {}", id); unlocked.add(id); - Minecraft mc = Minecraft.getInstance(); - if (mc.player != null) { mc.player.playSound(SoundEvents.EXPERIENCE_ORB_PICKUP, 1f, 1.2f); } - mc.getToasts().addToast( SystemToast.multiline( mc, @@ -43,6 +42,11 @@ public class SkilltreeClientState { unlocked.addAll(ids); } + public static void setUnlockedAbilities(Set abilities) { + unlockedAbilities.clear(); + unlockedAbilities.addAll(abilities); + } + public static void setCurrentSkillPoints(int points) { currentSkillPoints = points; } @@ -50,4 +54,44 @@ public class SkilltreeClientState { public static int getCurrentSkillPoints() { return currentSkillPoints; } + + public static boolean isAbilityUnlocked(ResourceLocation id) { + return unlockedAbilities.contains(id); + } + + + public static void unlockAbility(ResourceLocation id) { + unlockedAbilities.add(id); + } + + public static void bindAbilityLocal(int slot, ResourceLocation abilityId) { + int index = slot - 1; + if (index < 0 || index >= abilitySlots.length) return; + if (abilityId != null) { + for (int i = 0; i < abilitySlots.length; i++) { + if (abilityId.equals(abilitySlots[i])) abilitySlots[i] = null; + } + } + abilitySlots[index] = abilityId; + } + + public static void setAbilitySlots(ResourceLocation[] slots) { + for (int i = 0; i < abilitySlots.length; i++) { + abilitySlots[i] = (slots != null && i < slots.length) ? slots[i] : null; + } + } + + public static ResourceLocation getAbilityInSlot(int slot) { + int index = slot - 1; + if (index < 0 || index >= abilitySlots.length) return null; + return abilitySlots[index]; + } + + public static int getSlotOfAbility(ResourceLocation abilityId) { + if (abilityId == null) return -1; + for (int i = 0; i < abilitySlots.length; i++) { + if (abilityId.equals(abilitySlots[i])) return i + 1; + } + return -1; + } } \ No newline at end of file diff --git a/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java b/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java index 7c69d9e..0c8fed1 100644 --- a/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java +++ b/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java @@ -4,6 +4,8 @@ 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; @@ -14,7 +16,6 @@ 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.FormattedCharSequence; import net.minecraft.util.Mth; import java.util.ArrayList; @@ -22,18 +23,31 @@ 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 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 float zoom = 1.0f; - private float minZoom = 0.5f; - private float maxZoom = 2.0f; + private final float minZoom = 0.5f; + private final float maxZoom = 2.0f; private double panX = 0; private double panY = 0; private boolean dragging = false; - private double lastMouseX, lastMouseY; + private double lastMouseX; + private double lastMouseY; private SkillNode hoveredNode = null; private SkillNode selectedNode = null; + private AbilityNode hoveredAbility = null; + private AbilityNode selectedAbility = null; public SkilltreeScreen() { super(Component.literal("Ascend Skilltree")); @@ -46,34 +60,42 @@ public class SkilltreeScreen extends Screen { @Override public boolean mouseScrolled(double mouseX, double mouseY, double delta) { - zoom += delta * 0.1f; + zoom += delta * ZOOM_STEP; zoom = Mth.clamp(zoom, minZoom, maxZoom); return true; } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (selectedNode != null) { + if (selectedNode != null || selectedAbility != null) { return super.mouseClicked(mouseX, mouseY, button); } if (button == 0) { - SkillNode clicked = findNodeAt(mouseX, mouseY); - if (clicked != null) { - boolean lockedByExclusivity = clicked.getExclusiveWith().stream() - .anyMatch(SkilltreeClientState::isUnlocked); - if (lockedByExclusivity) { + SkillNode clickedNode = findNodeAt(mouseX, mouseY); + if (clickedNode != null) { + if (isLockedByExclusivity(clickedNode)) { Minecraft.getInstance().player.playSound( net.minecraft.sounds.SoundEvents.VILLAGER_NO, 1f, 1f ); return true; } - if (SkilltreeClientState.isUnlocked(clicked.getId())) { + if (SkilltreeClientState.isUnlocked(clickedNode.getId())) { return true; } - selectedNode = clicked; + 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; lastMouseX = mouseX; lastMouseY = mouseY; @@ -83,7 +105,7 @@ public class SkilltreeScreen extends Screen { @Override public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) { - if (selectedNode != null) { + if (selectedNode != null || selectedAbility != null) { return super.mouseDragged(mouseX, mouseY, button, dx, dy); } if (dragging && button == 0) { @@ -105,48 +127,26 @@ public class SkilltreeScreen extends Screen { @Override public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) { this.renderBackground(gfx); + renderSkillPointHeader(gfx); + hoveredNode = findNodeAt(mouseX, mouseY); + hoveredAbility = findAbilityAt(mouseX, mouseY); + drawGraph(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(remaining + " skillpoints remaining") .withStyle(ChatFormatting.YELLOW); gfx.drawString(font, pointsText, 10, 10, 0xFFFFFF); - - hoveredNode = findNodeAt(mouseX, mouseY); - drawGraph(gfx); - if (hoveredNode != null && selectedNode == null) { - List tooltip = new ArrayList<>(); - tooltip.add(hoveredNode.getTitle()); - String desc = hoveredNode.getDescription().getString(); - List wrapped = font.getSplitter().splitLines( - desc, - 220, - hoveredNode.getDescription().getStyle() - ); - for (FormattedText ft : wrapped) { - tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY)); - } - tooltip.add(Component.literal("Cost: " + hoveredNode.getCost()) - .withStyle(ChatFormatting.DARK_AQUA)); - if (!hoveredNode.getPrereqAttributes().isEmpty()) { - tooltip.add(Component.literal("Requirements:") - .withStyle(ChatFormatting.GOLD)); - Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS) - .ifPresent(stats -> { - hoveredNode.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) - ); - }); - }); - } - gfx.renderComponentTooltip(font, tooltip, mouseX, mouseY); - } - if (selectedNode != null) { - renderOverlay(gfx, selectedNode); - } - super.render(gfx, mouseX, mouseY, partialTicks); } private void drawGraph(GuiGraphics gfx) { @@ -161,35 +161,212 @@ public class SkilltreeScreen extends Screen { SkilltreeClientState.isUnlocked(neighbor.getId()); int linkColor = bothUnlocked ? 0xFF88FF88 : 0xFF666666; drawLine(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()); + drawLine(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6666AA); + } else if (ability2 != null) { + int[] p2 = worldToScreen(ability2.getX(), ability2.getY()); + drawLine(gfx, p1[0], p1[1], p2[0], p2[1], 0xFF6666AA); + } + } + } + for (AbilityNode ability : AbilityNodeRegistry.all()) { + int[] pos = worldToScreen(ability.getX(), ability.getY()); + boolean unlocked = SkilltreeClientState.isAbilityUnlocked(ability.getId()); + int color; + if (ability == selectedAbility) { + color = 0xFFAA8833; + } else if (ability == hoveredAbility) { + color = 0xFF8888AA; + } else if (unlocked) { + color = 0xFF3399FF; + } else { + color = 0xFF223355; + } + gfx.fill( + pos[0] - ABILITY_RADIUS, + pos[1] - ABILITY_RADIUS, + pos[0] + ABILITY_RADIUS, + pos[1] + ABILITY_RADIUS, + color + ); + ResourceLocation icon = ability.getIcon(); + if (icon != null) { + gfx.blit(icon, pos[0] - 8, pos[1] - 8, 0, 0, 16, 16, 16, 16); } } for (SkillNode node : SkillNodeRegistry.all()) { int[] pos = worldToScreen(node.getX(), node.getY()); - int r = 12; - boolean unlocked = SkilltreeClientState.isUnlocked(node.getId()); - int color = unlocked ? 0xFF44CC44 : 0xFF444444; - boolean lockedByExclusivity = node.getExclusiveWith().stream() - .anyMatch(SkilltreeClientState::isUnlocked); - if (lockedByExclusivity) { - color = 0xFFCC3333; - } - if (node == hoveredNode) color = 0xFF777777; - if (node == selectedNode) color = 0xFF999933; - gfx.fill(pos[0] - r, pos[1] - r, pos[0] + r, pos[1] + r, color); + int color = getNodeColor(node); + gfx.fill( + pos[0] - NODE_RADIUS, + pos[1] - NODE_RADIUS, + pos[0] + NODE_RADIUS, + pos[1] + NODE_RADIUS, + color + ); ResourceLocation icon = node.getIcons(); if (icon != null) { int size = 16; - gfx.blit(icon, + gfx.blit( + icon, pos[0] - size / 2, pos[1] - size / 2, 0, 0, size, size, - size, size); + size, size + ); } } } + private void renderNodeTooltip(GuiGraphics gfx, SkillNode node, int mouseX, int mouseY) { + List tooltip = new ArrayList<>(); + tooltip.add(node.getTitle()); + String desc = node.getDescription().getString(); + List wrapped = font.getSplitter().splitLines( + desc, + 220, + node.getDescription().getStyle() + ); + for (FormattedText ft : wrapped) { + tooltip.add(Component.literal(ft.getString()).withStyle(ChatFormatting.GRAY)); + } + tooltip.add(Component.literal("Cost: " + node.getCost()) + .withStyle(ChatFormatting.DARK_AQUA)); + if (!node.getPrereqAttributes().isEmpty()) { + tooltip.add(Component.literal("Requirements:") + .withStyle(ChatFormatting.GOLD)); + 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) + ); + })); + } + 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); + gfx.fill(x, y, x + OVERLAY_WIDTH, y + OVERLAY_HEIGHT, 0xFF222222); + 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; + 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); + gfx.fill(x, y, x + OVERLAY_WIDTH, y + OVERLAY_HEIGHT, 0xFF222222); + gfx.drawCenteredString(font, ability.getTitle(), x + OVERLAY_WIDTH / 2, y + 10, 0xFFFFFF); + if (ability.getDescription() != null) { + gfx.drawWordWrap(font, ability.getDescription(), x + 10, y + 30, OVERLAY_WIDTH - 20, 0xDDDDDD); + } + } + + 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 -> { + PacketHandler.CHANNEL.sendToServer( + new ServerboundUnlockNodePacket(selectedNode.getId()) + ); + selectedNode = null; + clearWidgets(); + }).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(); + }).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 -> { + PacketHandler.CHANNEL.sendToServer( + new ServerboundUnlockNodePacket(selectedAbility.getId()) + ); + selectedAbility = null; + clearWidgets(); + }).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(); + }).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; @@ -199,83 +376,51 @@ public class SkilltreeScreen extends Screen { private SkillNode findNodeAt(double mouseX, double mouseY) { for (SkillNode node : SkillNodeRegistry.all()) { int[] pos = worldToScreen(node.getX(), node.getY()); - int r = 12; - if (mouseX >= pos[0] - r && mouseX <= pos[0] + r && - mouseY >= pos[1] - r && mouseY <= pos[1] + r) { + if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], NODE_RADIUS)) { return node; } } return null; } - private void renderOverlay(GuiGraphics gfx, SkillNode node) { - boolean lockedByExclusivity = node.getExclusiveWith().stream() - .anyMatch(SkilltreeClientState::isUnlocked); - if (lockedByExclusivity) return; - int w = 240; - int h = 180; - int x = (width - w) / 2; - int y = (height - h) / 2; - gfx.fill(0, 0, width, height, 0x88000000); - gfx.fill(x, y, x + w, y + h, 0xFF222222); - gfx.drawCenteredString(font, node.getTitle(), x + w / 2, y + 10, 0xFFFFFF); - gfx.drawWordWrap(font, node.getDescription(), x + 10, y + 30, w - 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; - Minecraft.getInstance().player.getCapability(PlayerStatsProvider.PLAYER_STATS) - .ifPresent(stats -> { - for (var entry : node.getPrereqAttributes().entrySet()) { - String attr = entry.getKey(); - int required = entry.getValue(); - 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 AbilityNode findAbilityAt(double mouseX, double mouseY) { + for (AbilityNode ability : AbilityNodeRegistry.all()) { + int[] pos = worldToScreen(ability.getX(), ability.getY()); + if (isPointInCircle(mouseX, mouseY, pos[0], pos[1], ABILITY_RADIUS)) { + return ability; + } } + return null; } - private void rebuildOverlayButtons() { - clearWidgets(); - if (selectedNode == null) return; - int w = 240; - int h = 160; - int x = (width - w) / 2; - int y = (height - h) / 2; - boolean alreadyUnlocked = SkilltreeClientState.isUnlocked(selectedNode.getId()); - if (!alreadyUnlocked) { - addRenderableWidget(Button.builder(Component.literal("Unlock"), btn -> { - PacketHandler.CHANNEL.sendToServer( - new ServerboundUnlockNodePacket(selectedNode.getId()) - ); - selectedNode = null; - clearWidgets(); - }).pos(x + w / 2 - 40, y + h - 20).size(80, 20).build()); + 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 (lockedByExclusivity) { + color = 0xFFCC3333; + } else if (node == selectedNode) { + color = 0xFF999933; + } else if (node == hoveredNode) { + color = 0xFF777777; + } else if (unlocked) { + color = 0xFF44CC44; + } else { + color = 0xFF444444; } - addRenderableWidget(Button.builder(Component.literal("Close"), btn -> { - selectedNode = null; - clearWidgets(); - }).pos(x + w - 60, y - 5).size(50, 20).build()); + return color; } private void drawLine(GuiGraphics gfx, int x1, int y1, int x2, int y2, int color) { - float thickness = 2.0f; float dx = x2 - x1; float dy = y2 - y1; float length = (float) Math.sqrt(dx * dx + dy * dy); @@ -283,11 +428,13 @@ public class SkilltreeScreen extends Screen { gfx.pose().pushPose(); gfx.pose().translate(x1, y1, 0); gfx.pose().mulPose(Axis.ZP.rotation(angle)); - gfx.fill(0, - (int) (-thickness / 2f), + gfx.fill( + 0, + (int) (-LINE_THICKNESS / 2f), (int) length, - (int) (thickness / 2f), - color); + (int) (LINE_THICKNESS / 2f), + color + ); gfx.pose().popPose(); } diff --git a/src/main/java/net/fretux/skillengine/events/SkillEnginePlayerEvents.java b/src/main/java/net/fretux/skillengine/events/SkillEnginePlayerEvents.java index 4a99d94..169cf9b 100644 --- a/src/main/java/net/fretux/skillengine/events/SkillEnginePlayerEvents.java +++ b/src/main/java/net/fretux/skillengine/events/SkillEnginePlayerEvents.java @@ -17,7 +17,12 @@ public class SkillEnginePlayerEvents { player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { PacketHandler.CHANNEL.send( PacketDistributor.PLAYER.with(() -> player), - new ClientboundSyncSkillsPacket(data.getUnlockedNodes(), data.getSkillPoints()) + new ClientboundSyncSkillsPacket( + data.getUnlockedNodes(), + data.getUnlockedAbilities(), + data.getSkillPoints(), + data.getAbilitySlots() + ) ); }); } diff --git a/src/main/java/net/fretux/skillengine/network/ClientboundNodeUnlockedPacket.java b/src/main/java/net/fretux/skillengine/network/ClientboundNodeUnlockedPacket.java index f9d5ce8..16fc210 100644 --- a/src/main/java/net/fretux/skillengine/network/ClientboundNodeUnlockedPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ClientboundNodeUnlockedPacket.java @@ -1,5 +1,6 @@ package net.fretux.skillengine.network; +import net.fretux.skillengine.skilltree.AbilityNodeRegistry; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.network.NetworkEvent; @@ -30,8 +31,10 @@ public class ClientboundNodeUnlockedPacket { public static void handle(ClientboundNodeUnlockedPacket msg, Supplier ctx) { ctx.get().enqueueWork(() -> { - // Update client-side unlocked set and points immediately so UI can reflect changes this frame - SkilltreeClientState.unlockNode(msg.id); + if (AbilityNodeRegistry.get(msg.id) != null) + SkilltreeClientState.unlockAbility(msg.id); + else + SkilltreeClientState.unlockNode(msg.id); SkilltreeClientState.setCurrentSkillPoints(msg.newSkillPoints); }); ctx.get().setPacketHandled(true); diff --git a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java index 6e73be8..10d0868 100644 --- a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillsPacket.java @@ -12,35 +12,61 @@ import java.util.function.Supplier; public class ClientboundSyncSkillsPacket { private final Set unlocked; + private final Set unlockedAbilities; private final int skillPoints; + private final ResourceLocation[] abilitySlots; - public ClientboundSyncSkillsPacket(Set unlocked, int skillPoints) { + public ClientboundSyncSkillsPacket(Set unlocked, + Set unlockedAbilities, + int skillPoints, + ResourceLocation[] abilitySlots) { this.unlocked = unlocked; + this.unlockedAbilities = unlockedAbilities; this.skillPoints = skillPoints; + this.abilitySlots = abilitySlots; } public static void encode(ClientboundSyncSkillsPacket msg, FriendlyByteBuf buf) { buf.writeInt(msg.unlocked.size()); - for (ResourceLocation id : msg.unlocked) { - buf.writeResourceLocation(id); - } + for (ResourceLocation id : msg.unlocked) buf.writeResourceLocation(id); + buf.writeInt(msg.unlockedAbilities.size()); + for (ResourceLocation id : msg.unlockedAbilities) buf.writeResourceLocation(id); buf.writeInt(msg.skillPoints); + // ability slots (nullable entries) + int count = msg.abilitySlots != null ? msg.abilitySlots.length : 0; + buf.writeInt(count); + if (count > 0) { + for (int i = 0; i < count; i++) { + ResourceLocation id = msg.abilitySlots[i]; + buf.writeBoolean(id != null); + if (id != null) buf.writeResourceLocation(id); + } + } } public static ClientboundSyncSkillsPacket decode(FriendlyByteBuf buf) { int size = buf.readInt(); - Set result = new HashSet<>(); - for (int i = 0; i < size; i++) { - result.add(buf.readResourceLocation()); - } + Set unlocked = new HashSet<>(); + for (int i = 0; i < size; i++) unlocked.add(buf.readResourceLocation()); + int abilitySize = buf.readInt(); + Set unlockedAbilities = new HashSet<>(); + for (int i = 0; i < abilitySize; i++) unlockedAbilities.add(buf.readResourceLocation()); int points = buf.readInt(); - return new ClientboundSyncSkillsPacket(result, points); + int slotsCount = buf.readInt(); + ResourceLocation[] slots = new ResourceLocation[Math.max(slotsCount, 0)]; + for (int i = 0; i < slots.length; i++) { + boolean present = buf.readBoolean(); + slots[i] = present ? buf.readResourceLocation() : null; + } + return new ClientboundSyncSkillsPacket(unlocked, unlockedAbilities, points, slots); } - + public static void handle(ClientboundSyncSkillsPacket msg, Supplier ctx) { ctx.get().enqueueWork(() -> { SkilltreeClientState.setUnlocked(msg.unlocked); + SkilltreeClientState.setUnlockedAbilities(msg.unlockedAbilities); SkilltreeClientState.setCurrentSkillPoints(msg.skillPoints); + SkilltreeClientState.setAbilitySlots(msg.abilitySlots); }); ctx.get().setPacketHandled(true); } diff --git a/src/main/java/net/fretux/skillengine/network/PacketHandler.java b/src/main/java/net/fretux/skillengine/network/PacketHandler.java index 9846b02..b95e05a 100644 --- a/src/main/java/net/fretux/skillengine/network/PacketHandler.java +++ b/src/main/java/net/fretux/skillengine/network/PacketHandler.java @@ -37,12 +37,22 @@ public class PacketHandler { .decoder(ClientboundSyncSkillsPacket::decode) .consumerMainThread(ClientboundSyncSkillsPacket::handle) .add(); + CHANNEL.messageBuilder(ServerboundBindAbilityPacket.class, id++, NetworkDirection.PLAY_TO_SERVER) + .encoder(ServerboundBindAbilityPacket::encode) + .decoder(ServerboundBindAbilityPacket::decode) + .consumerMainThread(ServerboundBindAbilityPacket::handle) + .add(); } public static void syncSkillsTo(ServerPlayer player) { player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { ClientboundSyncSkillsPacket packet = - new ClientboundSyncSkillsPacket(data.getUnlockedNodes(), data.getSkillPoints()); + new ClientboundSyncSkillsPacket( + data.getUnlockedNodes(), + data.getUnlockedAbilities(), + data.getSkillPoints(), + data.getAbilitySlots() + ); CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), packet); }); } diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java new file mode 100644 index 0000000..3f53160 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java @@ -0,0 +1,44 @@ +package net.fretux.skillengine.network; + +import net.fretux.skillengine.capability.SkillEngineCapabilities; +import net.minecraft.server.level.ServerPlayer; +import net.fretux.skillengine.network.PacketHandler; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class ServerboundBindAbilityPacket { + + private final ResourceLocation abilityId; + private final int slot; + + public ServerboundBindAbilityPacket(ResourceLocation id, int slot) { + this.abilityId = id; + this.slot = slot; + } + + public static void encode(ServerboundBindAbilityPacket msg, FriendlyByteBuf buf) { + buf.writeResourceLocation(msg.abilityId); + buf.writeInt(msg.slot); + } + + public static ServerboundBindAbilityPacket decode(FriendlyByteBuf buf) { + return new ServerboundBindAbilityPacket(buf.readResourceLocation(), buf.readInt()); + } + + public static void handle(ServerboundBindAbilityPacket msg, Supplier ctx) { + ctx.get().enqueueWork(() -> { + ServerPlayer player = ctx.get().getSender(); + if (player == null) return; + + player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> + data.bindAbility(msg.slot, msg.abilityId) + ); + // Sync back to client so UI reflects updated bindings + PacketHandler.syncSkillsTo(player); + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java index 88ebcab..2baa2a0 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java @@ -2,9 +2,7 @@ package net.fretux.skillengine.network; import net.fretux.skillengine.SkillEngine; import net.fretux.skillengine.capability.SkillEngineCapabilities; -import net.fretux.skillengine.skilltree.SkillLogic; -import net.fretux.skillengine.skilltree.SkillNode; -import net.fretux.skillengine.skilltree.SkillNodeRegistry; +import net.fretux.skillengine.skilltree.*; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; @@ -37,27 +35,45 @@ public class ServerboundUnlockNodePacket { return; } SkillNode node = SkillNodeRegistry.get(msg.nodeId); - if (node == null) { - SkillEngine.LOGGER.warn("Unlock packet for unknown node {}", msg.nodeId); + AbilityNode ability = AbilityNodeRegistry.get(msg.nodeId); + if (node == null && ability == null) { + SkillEngine.LOGGER.warn("Unlock packet for unknown node or ability {}", msg.nodeId); return; } player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { - SkillEngine.LOGGER.info("Received unlock request for {} from {} (points={})", - msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints()); + if (node != null) { + SkillEngine.LOGGER.info("Received skill unlock request for {} from {} (points={})", + msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints()); + if (SkillLogic.canUnlock(data, player, node)) { + data.unlockNode(node); - if (SkillLogic.canUnlock(data, player, node)) { - data.unlockNode(node); - SkillEngine.LOGGER.info("Node {} unlocked for {}", msg.nodeId, player.getGameProfile().getName()); - PacketHandler.CHANNEL.send( - PacketDistributor.PLAYER.with(() -> player), - new ClientboundNodeUnlockedPacket(node.getId(), data.getSkillPoints()) - ); - } else { - SkillEngine.LOGGER.info("Cannot unlock node {} for {} (canUnlock=false)", - msg.nodeId, player.getGameProfile().getName()); + PacketHandler.CHANNEL.send( + PacketDistributor.PLAYER.with(() -> player), + new ClientboundNodeUnlockedPacket(node.getId(), data.getSkillPoints()) + ); + } else { + SkillEngine.LOGGER.info("Cannot unlock skill node {} for {}", + msg.nodeId, player.getGameProfile().getName()); + } + return; + } + if (ability != null) { + SkillEngine.LOGGER.info("Received ability unlock request for {} from {} (points={})", + msg.nodeId, player.getGameProfile().getName(), data.getSkillPoints()); + if (!data.isAbilityUnlocked(ability.getId())) { + data.unlockAbility(ability); + PacketHandler.CHANNEL.send( + PacketDistributor.PLAYER.with(() -> player), + new ClientboundNodeUnlockedPacket(ability.getId(), data.getSkillPoints()) + ); + } else { + SkillEngine.LOGGER.info("Ability {} already unlocked for {}", + msg.nodeId, player.getGameProfile().getName()); + } } }); }); + ctx.get().setPacketHandled(true); } } diff --git a/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java b/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java new file mode 100644 index 0000000..0c74e30 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java @@ -0,0 +1,48 @@ +package net.fretux.skillengine.skilltree; + +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +public class AbilityNode { + + private final ResourceLocation id; + private final Component title; + private final Component description; + private final float x, y; + private final List links; + private final List tags; + private final ResourceLocation icon; + private final int cooldown; + + public AbilityNode(ResourceLocation id, + Component title, + Component description, + float x, float y, + List links, + List tags, + ResourceLocation icon, + int cooldown) { + + this.id = id; + this.title = title; + this.description = description; + this.x = x; + this.y = y; + this.links = List.copyOf(links); + this.tags = List.copyOf(tags); + this.icon = icon; + this.cooldown = cooldown; + } + + public ResourceLocation getId() { return id; } + public Component getTitle() { return title; } + public Component getDescription() { return description; } + public float getX() { return x; } + public float getY() { return y; } + public List getLinks() { return links; } + public List getTags() { return tags; } + public ResourceLocation getIcon() { return icon; } + public int getCooldown() { return cooldown; } +} diff --git a/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeLoader.java b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeLoader.java new file mode 100644 index 0000000..674f4c6 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeLoader.java @@ -0,0 +1,71 @@ +package net.fretux.skillengine.skilltree; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import net.fretux.skillengine.SkillEngine; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraftforge.event.AddReloadListenerEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE) +public class AbilityNodeLoader extends SimpleJsonResourceReloadListener { + + private static final Gson GSON = new GsonBuilder().create(); + + public AbilityNodeLoader() { + super(GSON, "abilitynodes"); + } + + @SubscribeEvent + public static void onAddReloadListeners(AddReloadListenerEvent event) { + event.addListener(new AbilityNodeLoader()); + } + + @Override + protected void apply(Map jsons, + ResourceManager resourceManager, + ProfilerFiller profiler) { + + SkillEngine.LOGGER.info("Loading ability nodes..."); + + AbilityNodeRegistry.clear(); + + jsons.forEach((id, json) -> { + JsonObject obj = json.getAsJsonObject(); + + Component title = Component.literal(obj.get("title").getAsString()); + Component description = Component.literal(obj.get("description").getAsString()); + JsonObject pos = obj.getAsJsonObject("position"); + float x = pos.get("x").getAsFloat(); + float y = pos.get("y").getAsFloat(); + + List links = new ArrayList<>(); + obj.getAsJsonArray("links").forEach(e -> + links.add(new ResourceLocation(e.getAsString()))); + + List tags = new ArrayList<>(); + obj.getAsJsonArray("tags").forEach(e -> + tags.add(new ResourceLocation(e.getAsString()))); + + ResourceLocation icon = new ResourceLocation(obj.get("icon").getAsString()); + + int cooldown = obj.has("cooldown") ? obj.get("cooldown").getAsInt() : 0; + + AbilityNode node = new AbilityNode(id, title, description, x, y, links, tags, icon, cooldown); + AbilityNodeRegistry.put(node); + }); + + SkillEngine.LOGGER.info("Loaded {} active ability nodes", AbilityNodeRegistry.all().size()); + } +} diff --git a/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java new file mode 100644 index 0000000..0c5fbfa --- /dev/null +++ b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java @@ -0,0 +1,28 @@ +package net.fretux.skillengine.skilltree; + +import net.minecraft.resources.ResourceLocation; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +public class AbilityNodeRegistry { + + private static final Map ABILITIES = new LinkedHashMap<>(); + + public static void clear() { + ABILITIES.clear(); + } + + public static void put(AbilityNode node) { + ABILITIES.put(node.getId(), node); + } + + public static AbilityNode get(ResourceLocation id) { + return ABILITIES.get(id); + } + + public static Collection all() { + return ABILITIES.values(); + } +} diff --git a/src/main/resources/data/skillengine/abilitynodes/berserk.json b/src/main/resources/data/skillengine/abilitynodes/berserk.json new file mode 100644 index 0000000..603f78b --- /dev/null +++ b/src/main/resources/data/skillengine/abilitynodes/berserk.json @@ -0,0 +1,15 @@ +{ + "id": "skillengine:berserk", + "title": "Berserk", + "description": "Enter a frenzied state. Increased damage and speed.", + "position": { + "x": 110, + "y": 120 + }, + "links": [ + "skillengine:strength" + ], + "tags": ["skillengine:berserk"], + "icon": "minecraft:textures/item/blaze_powder.png", + "cooldown": 20 +} diff --git a/src/main/resources/data/skillengine/skillnodes/strength.json b/src/main/resources/data/skillengine/skillnodes/strength.json new file mode 100644 index 0000000..b4192f2 --- /dev/null +++ b/src/main/resources/data/skillengine/skillnodes/strength.json @@ -0,0 +1,17 @@ +{ + "id": "skillengine:strength", + "title": "Strength", + "description": "Stronk", + "cost": 0, + "position": { + "x": 45, + "y": 45 + }, + "links": ["skillengine:welcome"], + "tags": [ + "skillengine:strength", + "skillengine:stronk" + ], + "icons": "minecraft:textures/item/book.png", + "prerequisites": {} +} \ No newline at end of file