From 77fbada43851f5250be96f092b51a8fad649e417 Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 25 May 2026 12:33:49 +0200 Subject: [PATCH] extend ability system to support dynamic slot counts, add skill tree validation, new prerequisites API, and extended mod commands --- .../net/fretux/skillengine/SkillEngine.java | 4 + .../abilities/AbilityCooldownHandler.java | 2 +- .../skillengine/api/AbilityHandler.java | 9 + .../api/AbilityHandlerRegistry.java | 38 ++++ .../skillengine/api/PrerequisiteProvider.java | 9 + .../api/PrerequisiteProviderRegistry.java | 38 ++++ .../skillengine/api/SkillEngineAPI.java | 71 +++++++- .../capability/PlayerSkillData.java | 89 ++++++++- .../client/AbilityBindingScreen.java | 32 ++-- .../skillengine/client/AbilityHudOverlay.java | 3 +- .../client/AbilityKeybindHandler.java | 56 +++--- .../client/AbilitySlotsScreen.java | 39 ++-- .../client/ClientCooldownTicker.java | 7 +- .../skillengine/client/ClientKeybinds.java | 6 +- .../client/SkilltreeClientState.java | 16 +- .../skillengine/client/SkilltreeScreen.java | 47 ++++- .../commands/SkillEngineCommands.java | 45 +++++ .../skillengine/config/SkillEngineConfig.java | 19 ++ .../events/SkillPointsChangedEvent.java | 42 +++++ ...ClientboundSyncSkillDefinitionsPacket.java | 163 +++++++++++++++++ .../skillengine/network/PacketHandler.java | 2 +- .../ServerboundActivateAbilityPacket.java | 2 +- .../network/ServerboundBindAbilityPacket.java | 2 +- .../network/ServerboundUnlockNodePacket.java | 10 + .../skillengine/skilltree/AbilityNode.java | 5 +- .../skilltree/AbilityNodeRegistry.java | 14 +- .../skillengine/skilltree/SkillLogic.java | 15 +- .../skillengine/skilltree/SkillNode.java | 23 ++- .../skilltree/SkillNodeLoader.java | 68 ++++--- .../skilltree/SkillNodeRegistry.java | 32 +++- .../skilltree/SkillTreeValidator.java | 172 ++++++++++++++++++ .../assets/skillengine/lang/en_us.json | 9 +- 32 files changed, 961 insertions(+), 128 deletions(-) create mode 100644 src/main/java/net/fretux/skillengine/api/AbilityHandler.java create mode 100644 src/main/java/net/fretux/skillengine/api/AbilityHandlerRegistry.java create mode 100644 src/main/java/net/fretux/skillengine/api/PrerequisiteProvider.java create mode 100644 src/main/java/net/fretux/skillengine/api/PrerequisiteProviderRegistry.java create mode 100644 src/main/java/net/fretux/skillengine/commands/SkillEngineCommands.java create mode 100644 src/main/java/net/fretux/skillengine/config/SkillEngineConfig.java create mode 100644 src/main/java/net/fretux/skillengine/events/SkillPointsChangedEvent.java create mode 100644 src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java create mode 100644 src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java diff --git a/src/main/java/net/fretux/skillengine/SkillEngine.java b/src/main/java/net/fretux/skillengine/SkillEngine.java index d3a82f4..8681608 100644 --- a/src/main/java/net/fretux/skillengine/SkillEngine.java +++ b/src/main/java/net/fretux/skillengine/SkillEngine.java @@ -1,10 +1,13 @@ package net.fretux.skillengine; import com.mojang.logging.LogUtils; +import net.fretux.skillengine.config.SkillEngineConfig; import net.minecraftforge.api.distmarker.Dist; import net.fretux.skillengine.network.PacketHandler; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.slf4j.Logger; @@ -17,6 +20,7 @@ public class SkillEngine { public SkillEngine() { IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); + ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, SkillEngineConfig.COMMON_SPEC); PacketHandler.register(); DistExecutor.unsafeRunWhenOn( Dist.CLIENT, diff --git a/src/main/java/net/fretux/skillengine/abilities/AbilityCooldownHandler.java b/src/main/java/net/fretux/skillengine/abilities/AbilityCooldownHandler.java index 47a8e2b..32d542e 100644 --- a/src/main/java/net/fretux/skillengine/abilities/AbilityCooldownHandler.java +++ b/src/main/java/net/fretux/skillengine/abilities/AbilityCooldownHandler.java @@ -23,7 +23,7 @@ public class AbilityCooldownHandler { data.tickCooldowns(); data.tickTagCooldowns(); if (player.tickCount % 10 == 0) { - for (int i = 1; i <= 3; i++) { + for (int i = 1; i <= data.getAbilitySlots().length; i++) { PacketHandler.CHANNEL.send( PacketDistributor.PLAYER.with(() -> player), new ClientboundSyncCooldownPacket(i, data.getCooldown(i)) diff --git a/src/main/java/net/fretux/skillengine/api/AbilityHandler.java b/src/main/java/net/fretux/skillengine/api/AbilityHandler.java new file mode 100644 index 0000000..31ff128 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/api/AbilityHandler.java @@ -0,0 +1,9 @@ +package net.fretux.skillengine.api; + +import net.fretux.skillengine.skilltree.AbilityNode; +import net.minecraft.server.level.ServerPlayer; + +@FunctionalInterface +public interface AbilityHandler { + boolean execute(ServerPlayer player, AbilityNode ability); +} diff --git a/src/main/java/net/fretux/skillengine/api/AbilityHandlerRegistry.java b/src/main/java/net/fretux/skillengine/api/AbilityHandlerRegistry.java new file mode 100644 index 0000000..983e84b --- /dev/null +++ b/src/main/java/net/fretux/skillengine/api/AbilityHandlerRegistry.java @@ -0,0 +1,38 @@ +package net.fretux.skillengine.api; + +import net.fretux.skillengine.skilltree.AbilityNode; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class AbilityHandlerRegistry { + + private static final Map BY_ID = new LinkedHashMap<>(); + private static final Map BY_TAG = new LinkedHashMap<>(); + + private AbilityHandlerRegistry() {} + + public static void register(ResourceLocation abilityId, AbilityHandler handler) { + BY_ID.put(abilityId, handler); + } + + public static void registerForTag(ResourceLocation tag, AbilityHandler handler) { + BY_TAG.put(tag, handler); + } + + public static boolean execute(ServerPlayer player, AbilityNode ability) { + AbilityHandler direct = BY_ID.get(ability.getId()); + if (direct != null) { + return direct.execute(player, ability); + } + for (ResourceLocation tag : ability.getTags()) { + AbilityHandler tagged = BY_TAG.get(tag); + if (tagged != null && tagged.execute(player, ability)) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/net/fretux/skillengine/api/PrerequisiteProvider.java b/src/main/java/net/fretux/skillengine/api/PrerequisiteProvider.java new file mode 100644 index 0000000..ec729ae --- /dev/null +++ b/src/main/java/net/fretux/skillengine/api/PrerequisiteProvider.java @@ -0,0 +1,9 @@ +package net.fretux.skillengine.api; + +import net.fretux.skillengine.skilltree.SkillNode; +import net.minecraft.server.level.ServerPlayer; + +@FunctionalInterface +public interface PrerequisiteProvider { + boolean meets(ServerPlayer player, SkillNode node, String key, int requiredValue); +} diff --git a/src/main/java/net/fretux/skillengine/api/PrerequisiteProviderRegistry.java b/src/main/java/net/fretux/skillengine/api/PrerequisiteProviderRegistry.java new file mode 100644 index 0000000..fd80b95 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/api/PrerequisiteProviderRegistry.java @@ -0,0 +1,38 @@ +package net.fretux.skillengine.api; + +import net.fretux.ascend.player.PlayerStatsProvider; +import net.fretux.skillengine.skilltree.SkillNode; +import net.minecraft.server.level.ServerPlayer; + +import java.util.LinkedHashMap; +import java.util.Map; + +public final class PrerequisiteProviderRegistry { + + private static final Map PROVIDERS = new LinkedHashMap<>(); + + static { + register("ascend", (player, node, key, requiredValue) -> + player.getCapability(PlayerStatsProvider.PLAYER_STATS) + .map(stats -> stats.getAttributeLevel(key) >= requiredValue) + .orElse(false)); + } + + private PrerequisiteProviderRegistry() {} + + public static void register(String namespace, PrerequisiteProvider provider) { + PROVIDERS.put(namespace, provider); + } + + public static boolean meets(ServerPlayer player, SkillNode node, String key, int requiredValue) { + int separator = key.indexOf(':'); + String namespace = separator > 0 ? key.substring(0, separator) : "ascend"; + String localKey = separator > 0 ? key.substring(separator + 1) : key; + PrerequisiteProvider provider = PROVIDERS.get(namespace); + return provider != null && provider.meets(player, node, localKey, requiredValue); + } + + public static boolean hasProvider(String namespace) { + return PROVIDERS.containsKey(namespace); + } +} diff --git a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java index 5fc5243..126ff9b 100644 --- a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java +++ b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java @@ -4,6 +4,7 @@ import net.fretux.skillengine.SkillEngine; import net.fretux.skillengine.capability.PlayerSkillData; import net.fretux.skillengine.capability.SkillEngineCapabilities; import net.fretux.skillengine.client.ClientSkillEngineBridge; +import net.fretux.skillengine.events.SkillPointsChangedEvent; import net.fretux.skillengine.skilltree.AbilityNode; import net.fretux.skillengine.skilltree.AbilityNodeRegistry; import net.fretux.skillengine.skilltree.SkillLogic; @@ -12,6 +13,7 @@ import net.fretux.skillengine.skilltree.SkillNodeRegistry; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.DistExecutor; import net.minecraftforge.fml.loading.FMLEnvironment; @@ -65,7 +67,17 @@ public final class SkillEngineAPI { public static void addSkillPoints(Player player, int amount) { PlayerSkillData data = getPlayerData(player); - if (data != null) data.addSkillPoints(amount); + if (data != null) { + data.addSkillPoints(amount); + if (amount != 0) { + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + Math.abs(amount), + amount > 0 ? SkillPointsChangedEvent.Reason.GAINED : SkillPointsChangedEvent.Reason.SPENT, + null + )); + } + } } public static boolean consumeSkillPoints(Player player, int amount) { @@ -73,6 +85,12 @@ public final class SkillEngineAPI { PlayerSkillData data = getPlayerData(player); if (data != null && data.getSkillPoints() >= amount) { data.addSkillPoints(-amount); + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + amount, + SkillPointsChangedEvent.Reason.SPENT, + null + )); return true; } return false; @@ -97,10 +115,61 @@ public final class SkillEngineAPI { if (unlockPlan == null) return false; for (SkillNode plannedNode : unlockPlan) { data.unlockNode(plannedNode); + if (plannedNode.getCost() > 0) { + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + plannedNode.getCost(), + SkillPointsChangedEvent.Reason.SPENT, + plannedNode.getId() + )); + } } return true; } + public static boolean refundSkill(Player player, ResourceLocation id) { + PlayerSkillData data = getPlayerData(player); + SkillNode node = SkillNodeRegistry.get(id); + if (data == null || node == null || !data.refundNode(id)) return false; + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + node.getCost(), + SkillPointsChangedEvent.Reason.REFUNDED, + id + )); + return true; + } + + public static int refundBranch(Player player, ResourceLocation rootId) { + PlayerSkillData data = getPlayerData(player); + if (data == null) return 0; + int refunded = data.refundBranch(rootId); + if (refunded > 0) { + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + refunded, + SkillPointsChangedEvent.Reason.REFUNDED, + rootId + )); + } + return refunded; + } + + public static int resetSkills(Player player) { + PlayerSkillData data = getPlayerData(player); + if (data == null) return 0; + int refunded = data.resetAllNodesPreserveEarnedPoints(); + if (refunded > 0) { + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + refunded, + SkillPointsChangedEvent.Reason.REFUNDED, + null + )); + } + return refunded; + } + public static boolean isAbilityUnlocked(Player player, ResourceLocation id) { PlayerSkillData data = getPlayerData(player); return data != null && data.isAbilityUnlocked(id); diff --git a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java index 40e56c8..00ea730 100644 --- a/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java +++ b/src/main/java/net/fretux/skillengine/capability/PlayerSkillData.java @@ -1,6 +1,7 @@ package net.fretux.skillengine.capability; import net.fretux.skillengine.network.PacketHandler; +import net.fretux.skillengine.config.SkillEngineConfig; import net.fretux.skillengine.skilltree.AbilityNode; import net.fretux.skillengine.skilltree.SkillNode; import net.fretux.skillengine.skilltree.SkillNodeRegistry; @@ -21,8 +22,8 @@ public class PlayerSkillData { private final Set unlockedNodes = new HashSet<>(); private final Set unlockedAbilities = new HashSet<>(); private final Set activeTags = new HashSet<>(); - private final ResourceLocation[] abilitySlots = new ResourceLocation[3]; - private final int[] abilityCooldowns = new int[3]; + private ResourceLocation[] abilitySlots = new ResourceLocation[getConfiguredSlotCount()]; + private int[] abilityCooldowns = new int[abilitySlots.length]; private final Map tagCooldowns = new HashMap<>(); public boolean isOnCooldown(ResourceLocation tag) { @@ -84,6 +85,7 @@ public class PlayerSkillData { } public void bindAbility(int slot, ResourceLocation abilityId) { + ensureSlotCount(); bindHelper(slot, abilityId, abilitySlots); } @@ -101,16 +103,19 @@ public class PlayerSkillData { } public ResourceLocation getAbilityInSlot(int slot) { + ensureSlotCount(); int index = slot - 1; if (index < 0 || index >= abilitySlots.length) return null; return abilitySlots[index]; } public ResourceLocation[] getAbilitySlots() { + ensureSlotCount(); return abilitySlots.clone(); } public void clearAbilitySlots() { + ensureSlotCount(); for (int i = 0; i < abilitySlots.length; i++) { abilitySlots[i] = null; abilityCooldowns[i] = 0; @@ -118,18 +123,21 @@ public class PlayerSkillData { } public void setCooldown(int slot, int ticks) { + ensureSlotCount(); int idx = slot - 1; if (idx < 0 || idx >= abilityCooldowns.length) return; abilityCooldowns[idx] = ticks; } public int getCooldown(int slot) { + ensureSlotCount(); int idx = slot - 1; if (idx < 0 || idx >= abilityCooldowns.length) return 0; return abilityCooldowns[idx]; } public void tickCooldowns() { + ensureSlotCount(); for (int i = 0; i < abilityCooldowns.length; i++) { if (abilityCooldowns[i] > 0) { abilityCooldowns[i]--; @@ -155,6 +163,7 @@ public class PlayerSkillData { } tag.put("ActiveTags", tagsList); ListTag slotList = new ListTag(); + ensureSlotCount(); for (ResourceLocation slot : abilitySlots) { CompoundTag t = new CompoundTag(); t.putString("Id", slot == null ? "" : slot.toString()); @@ -183,8 +192,8 @@ public class PlayerSkillData { unlockedNodes.clear(); unlockedAbilities.clear(); activeTags.clear(); - Arrays.fill(abilitySlots, null); - Arrays.fill(abilityCooldowns, 0); + abilitySlots = new ResourceLocation[getConfiguredSlotCount()]; + abilityCooldowns = new int[abilitySlots.length]; tagCooldowns.clear(); ListTag nodeList = tag.getList("UnlockedNodes", Tag.TAG_COMPOUND); for (Tag t : nodeList) { @@ -198,7 +207,7 @@ public class PlayerSkillData { activeTags.add(ResourceLocation.parse(((CompoundTag) t).getString("Tag"))); } ListTag slots = tag.getList("AbilitySlots", Tag.TAG_COMPOUND); - for (int i = 0; i < 3 && i < slots.size(); i++) { + for (int i = 0; i < abilitySlots.length && i < slots.size(); i++) { String id = slots.getCompound(i).getString("Id"); abilitySlots[i] = id.isEmpty() ? null : ResourceLocation.parse(id); } @@ -229,6 +238,76 @@ public class PlayerSkillData { clearAbilitySlots(); } + public boolean refundNode(ResourceLocation nodeId) { + SkillNode node = SkillNodeRegistry.get(nodeId); + if (node == null || !unlockedNodes.remove(nodeId)) return false; + for (SkillNode other : SkillNodeRegistry.all()) { + if (unlockedNodes.contains(other.getId()) && other.getLinks().contains(nodeId)) { + unlockedNodes.add(nodeId); + return false; + } + } + skillPoints += node.getCost(); + rebuildActiveTags(); + return true; + } + + public int refundBranch(ResourceLocation rootId) { + int refunded = 0; + Set toRefund = new HashSet<>(); + collectChildren(rootId, toRefund); + for (ResourceLocation id : toRefund) { + SkillNode node = SkillNodeRegistry.get(id); + if (node != null && unlockedNodes.remove(id)) { + skillPoints += node.getCost(); + refunded += node.getCost(); + } + } + rebuildActiveTags(); + return refunded; + } + + public int resetAllNodesPreserveEarnedPoints() { + int refunded = getTotalSkillCost(); + skillPoints += refunded; + clearAllNodes(); + return refunded; + } + + private void rebuildActiveTags() { + activeTags.clear(); + for (ResourceLocation id : unlockedNodes) { + SkillNode node = SkillNodeRegistry.get(id); + if (node != null) activeTags.addAll(node.getTags()); + } + } + + private void collectChildren(ResourceLocation rootId, Set out) { + if (!out.add(rootId)) return; + for (SkillNode node : SkillNodeRegistry.all()) { + if (node.getLinks().contains(rootId)) { + collectChildren(node.getId(), out); + } + } + } + + private static int getConfiguredSlotCount() { + try { + return SkillEngineConfig.ABILITY_SLOT_COUNT.get(); + } catch (IllegalStateException ignored) { + return 3; + } + } + + private void ensureSlotCount() { + int configured = getConfiguredSlotCount(); + if (abilitySlots.length == configured) return; + ResourceLocation[] oldSlots = abilitySlots; + int[] oldCooldowns = abilityCooldowns; + abilitySlots = Arrays.copyOf(oldSlots, configured); + abilityCooldowns = Arrays.copyOf(oldCooldowns, configured); + } + public void sync(ServerPlayer player) { PacketHandler.syncSkillsTo(player); } diff --git a/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java b/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java index d45c0cc..8deb5ab 100644 --- a/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java +++ b/src/main/java/net/fretux/skillengine/client/AbilityBindingScreen.java @@ -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()); diff --git a/src/main/java/net/fretux/skillengine/client/AbilityHudOverlay.java b/src/main/java/net/fretux/skillengine/client/AbilityHudOverlay.java index 265832d..481832e 100644 --- a/src/main/java/net/fretux/skillengine/client/AbilityHudOverlay.java +++ b/src/main/java/net/fretux/skillengine/client/AbilityHudOverlay.java @@ -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); diff --git a/src/main/java/net/fretux/skillengine/client/AbilityKeybindHandler.java b/src/main/java/net/fretux/skillengine/client/AbilityKeybindHandler.java index a232fb1..2ac8c0e 100644 --- a/src/main/java/net/fretux/skillengine/client/AbilityKeybindHandler.java +++ b/src/main/java/net/fretux/skillengine/client/AbilityKeybindHandler.java @@ -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(); + }; + } } diff --git a/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java b/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java index 98f7bea..e23ed8d 100644 --- a/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java +++ b/src/main/java/net/fretux/skillengine/client/AbilitySlotsScreen.java @@ -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); + } } diff --git a/src/main/java/net/fretux/skillengine/client/ClientCooldownTicker.java b/src/main/java/net/fretux/skillengine/client/ClientCooldownTicker.java index 7522bbe..c8ba550 100644 --- a/src/main/java/net/fretux/skillengine/client/ClientCooldownTicker.java +++ b/src/main/java/net/fretux/skillengine/client/ClientCooldownTicker.java @@ -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); } } } diff --git a/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java b/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java index d1fd013..86e916d 100644 --- a/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java +++ b/src/main/java/net/fretux/skillengine/client/ClientKeybinds.java @@ -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); + } } } diff --git a/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java b/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java index 037bcfb..eaf9569 100644 --- a/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java +++ b/src/main/java/net/fretux/skillengine/client/SkilltreeClientState.java @@ -14,16 +14,18 @@ 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]; - 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); + } } diff --git a/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java b/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java index 0399f39..a725594 100644 --- a/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java +++ b/src/main/java/net/fretux/skillengine/client/SkilltreeScreen.java @@ -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 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 activeSkillNodes() { + return activeTree != null ? SkillNodeRegistry.byTree(activeTree) : SkillNodeRegistry.all(); + } + + private void rebuildTreeTabs() { + List trees = SkillNodeRegistry.trees(); + if (trees.size() <= 1 || selectedNode != null || selectedAbility != null) { + return; + } + int x = 10; + int y = 28; + for (ResourceLocation tree : trees) { + String label = tree.toString(); + int buttonWidth = Math.min(140, Math.max(60, font.width(label) + 16)); + Button button = addRenderableWidget(Button.builder(Component.literal(label), btn -> { + activeTree = tree; + selectedNode = null; + selectedAbility = null; + clearWidgets(); + rebuildTreeTabs(); + }) + .pos(x, y) + .size(buttonWidth, 20) + .build()); + button.active = !tree.equals(activeTree); + x += buttonWidth + 4; + } + } + @Override public boolean isPauseScreen() { return false; } -} \ No newline at end of file +} diff --git a/src/main/java/net/fretux/skillengine/commands/SkillEngineCommands.java b/src/main/java/net/fretux/skillengine/commands/SkillEngineCommands.java new file mode 100644 index 0000000..0a19d33 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/commands/SkillEngineCommands.java @@ -0,0 +1,45 @@ +package net.fretux.skillengine.commands; + +import com.mojang.brigadier.CommandDispatcher; +import net.fretux.skillengine.SkillEngine; +import net.fretux.skillengine.skilltree.SkillTreeValidator; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +import java.util.List; + +@Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE) +public final class SkillEngineCommands { + + private SkillEngineCommands() {} + + @SubscribeEvent + public static void register(RegisterCommandsEvent event) { + register(event.getDispatcher()); + } + + private static void register(CommandDispatcher dispatcher) { + dispatcher.register(Commands.literal("skillengine") + .requires(source -> source.hasPermission(2)) + .then(Commands.literal("validate") + .executes(context -> validate(context.getSource())))); + } + + private static int validate(CommandSourceStack source) { + List issues = SkillTreeValidator.validate(); + if (issues.isEmpty()) { + source.sendSuccess(() -> Component.literal("Skill Engine validation passed."), false); + return 1; + } + source.sendFailure(Component.literal("Skill Engine validation found " + issues.size() + " issue(s):")); + for (String issue : issues) { + SkillEngine.LOGGER.warn("[SkillEngine validate] {}", issue); + source.sendFailure(Component.literal("- " + issue)); + } + return 0; + } +} diff --git a/src/main/java/net/fretux/skillengine/config/SkillEngineConfig.java b/src/main/java/net/fretux/skillengine/config/SkillEngineConfig.java new file mode 100644 index 0000000..e03da88 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/config/SkillEngineConfig.java @@ -0,0 +1,19 @@ +package net.fretux.skillengine.config; + +import net.minecraftforge.common.ForgeConfigSpec; + +public final class SkillEngineConfig { + + public static final ForgeConfigSpec COMMON_SPEC; + public static final ForgeConfigSpec.IntValue ABILITY_SLOT_COUNT; + + static { + ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); + ABILITY_SLOT_COUNT = builder + .comment("Number of ability slots available to each player.") + .defineInRange("abilitySlotCount", 3, 1, 9); + COMMON_SPEC = builder.build(); + } + + private SkillEngineConfig() {} +} diff --git a/src/main/java/net/fretux/skillengine/events/SkillPointsChangedEvent.java b/src/main/java/net/fretux/skillengine/events/SkillPointsChangedEvent.java new file mode 100644 index 0000000..0a99c9e --- /dev/null +++ b/src/main/java/net/fretux/skillengine/events/SkillPointsChangedEvent.java @@ -0,0 +1,42 @@ +package net.fretux.skillengine.events; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraftforge.eventbus.api.Event; + +public class SkillPointsChangedEvent extends Event { + + public enum Reason { + GAINED, + SPENT, + REFUNDED + } + + private final Player player; + private final int amount; + private final Reason reason; + private final ResourceLocation nodeId; + + public SkillPointsChangedEvent(Player player, int amount, Reason reason, ResourceLocation nodeId) { + this.player = player; + this.amount = amount; + this.reason = reason; + this.nodeId = nodeId; + } + + public Player getPlayer() { + return player; + } + + public int getAmount() { + return amount; + } + + public Reason getReason() { + return reason; + } + + public ResourceLocation getNodeId() { + return nodeId; + } +} diff --git a/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java new file mode 100644 index 0000000..6488241 --- /dev/null +++ b/src/main/java/net/fretux/skillengine/network/ClientboundSyncSkillDefinitionsPacket.java @@ -0,0 +1,163 @@ +package net.fretux.skillengine.network; + +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.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkEvent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class ClientboundSyncSkillDefinitionsPacket { + + private final List skillNodes; + private final List abilityNodes; + + public ClientboundSyncSkillDefinitionsPacket(Collection skillNodes, + Collection abilityNodes) { + this.skillNodes = List.copyOf(skillNodes); + this.abilityNodes = List.copyOf(abilityNodes); + } + + public static void encode(ClientboundSyncSkillDefinitionsPacket msg, FriendlyByteBuf buf) { + buf.writeInt(msg.skillNodes.size()); + for (SkillNode node : msg.skillNodes) { + writeSkillNode(buf, node); + } + + buf.writeInt(msg.abilityNodes.size()); + for (AbilityNode node : msg.abilityNodes) { + writeAbilityNode(buf, node); + } + } + + public static ClientboundSyncSkillDefinitionsPacket decode(FriendlyByteBuf buf) { + int skillNodeCount = buf.readInt(); + List skillNodes = new ArrayList<>(skillNodeCount); + for (int i = 0; i < skillNodeCount; i++) { + skillNodes.add(readSkillNode(buf)); + } + + int abilityNodeCount = buf.readInt(); + List abilityNodes = new ArrayList<>(abilityNodeCount); + for (int i = 0; i < abilityNodeCount; i++) { + abilityNodes.add(readAbilityNode(buf)); + } + + return new ClientboundSyncSkillDefinitionsPacket(skillNodes, abilityNodes); + } + + public static void handle(ClientboundSyncSkillDefinitionsPacket msg, Supplier ctx) { + ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn( + Dist.CLIENT, + () -> () -> net.fretux.skillengine.client.ClientSkillEngineBridge.handleSkillDefinitionsSync( + msg.skillNodes, + msg.abilityNodes + ) + )); + ctx.get().setPacketHandled(true); + } + + private static void writeSkillNode(FriendlyByteBuf buf, SkillNode node) { + buf.writeResourceLocation(node.getId()); + buf.writeComponent(node.getTitle()); + buf.writeComponent(node.getDescription()); + buf.writeInt(node.getCost()); + buf.writeFloat(node.getX()); + buf.writeFloat(node.getY()); + writeResourceLocationList(buf, node.getLinks()); + writeResourceLocationList(buf, node.getTags()); + buf.writeResourceLocation(node.getTree()); + buf.writeUtf(node.getCategory()); + buf.writeUtf(node.getLayer()); + buf.writeResourceLocation(node.getIcons()); + writePrerequisites(buf, node.getPrereqAttributes()); + writeResourceLocationList(buf, node.getExclusiveWith()); + } + + private static SkillNode readSkillNode(FriendlyByteBuf buf) { + ResourceLocation id = buf.readResourceLocation(); + Component title = buf.readComponent(); + Component description = buf.readComponent(); + int cost = buf.readInt(); + float x = buf.readFloat(); + float y = buf.readFloat(); + List links = readResourceLocationList(buf); + List tags = readResourceLocationList(buf); + ResourceLocation tree = buf.readResourceLocation(); + String category = buf.readUtf(); + String layer = buf.readUtf(); + ResourceLocation icon = buf.readResourceLocation(); + Map prerequisites = readPrerequisites(buf); + List exclusiveWith = readResourceLocationList(buf); + return new SkillNode(id, title, description, cost, x, y, links, tags, tree, category, layer, icon, prerequisites, exclusiveWith); + } + + private static void writeAbilityNode(FriendlyByteBuf buf, AbilityNode node) { + buf.writeResourceLocation(node.getId()); + buf.writeComponent(node.getTitle()); + buf.writeComponent(node.getDescription()); + buf.writeFloat(node.getX()); + buf.writeFloat(node.getY()); + writeResourceLocationList(buf, node.getLinks()); + writeResourceLocationList(buf, node.getTags()); + buf.writeResourceLocation(node.getIcon()); + buf.writeInt(node.getCooldown()); + } + + private static AbilityNode readAbilityNode(FriendlyByteBuf buf) { + ResourceLocation id = buf.readResourceLocation(); + Component title = buf.readComponent(); + Component description = buf.readComponent(); + float x = buf.readFloat(); + float y = buf.readFloat(); + List links = readResourceLocationList(buf); + List tags = readResourceLocationList(buf); + ResourceLocation icon = buf.readResourceLocation(); + int cooldown = buf.readInt(); + return new AbilityNode(id, title, description, x, y, links, tags, icon, cooldown); + } + + private static void writeResourceLocationList(FriendlyByteBuf buf, List ids) { + buf.writeInt(ids.size()); + for (ResourceLocation id : ids) { + buf.writeResourceLocation(id); + } + } + + private static List readResourceLocationList(FriendlyByteBuf buf) { + int size = buf.readInt(); + List ids = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + ids.add(buf.readResourceLocation()); + } + return ids; + } + + private static void writePrerequisites(FriendlyByteBuf buf, Map prerequisites) { + buf.writeInt(prerequisites.size()); + for (Map.Entry entry : prerequisites.entrySet()) { + buf.writeUtf(entry.getKey()); + buf.writeInt(entry.getValue()); + } + } + + private static Map readPrerequisites(FriendlyByteBuf buf) { + int size = buf.readInt(); + Map prerequisites = new HashMap<>(size); + for (int i = 0; i < size; i++) { + prerequisites.put(buf.readUtf(), buf.readInt()); + } + return prerequisites; + } +} diff --git a/src/main/java/net/fretux/skillengine/network/PacketHandler.java b/src/main/java/net/fretux/skillengine/network/PacketHandler.java index 3ac1e34..9571eaf 100644 --- a/src/main/java/net/fretux/skillengine/network/PacketHandler.java +++ b/src/main/java/net/fretux/skillengine/network/PacketHandler.java @@ -13,7 +13,7 @@ import net.minecraftforge.network.simple.SimpleChannel; public class PacketHandler { - private static final String PROTOCOL = "3"; + private static final String PROTOCOL = "4"; public static final SimpleChannel CHANNEL = NetworkRegistry.ChannelBuilder .named(ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "network")) .clientAcceptedVersions(PROTOCOL::equals) diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java index 91be9e3..975a5d7 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundActivateAbilityPacket.java @@ -34,7 +34,7 @@ public class ServerboundActivateAbilityPacket { if (player == null) return; player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { - if (msg.slot < 1 || msg.slot > 3) return; + if (msg.slot < 1 || msg.slot > data.getAbilitySlots().length) return; ResourceLocation abilityId = data.getAbilityInSlot(msg.slot); if (abilityId == null) return; diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java index 4185071..aecb065 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundBindAbilityPacket.java @@ -34,7 +34,7 @@ public class ServerboundBindAbilityPacket { if (player == null) return; player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { - if (msg.slot < 1 || msg.slot > 3) return; + if (msg.slot < 1 || msg.slot > data.getAbilitySlots().length) return; if (AbilityNodeRegistry.get(msg.abilityId) == null) return; if (!data.isAbilityUnlocked(msg.abilityId)) return; data.bindAbility(msg.slot, msg.abilityId); diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java index 8159a07..352162c 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java @@ -3,10 +3,12 @@ package net.fretux.skillengine.network; import net.fretux.skillengine.SkillEngine; import net.fretux.skillengine.capability.PlayerSkillData; import net.fretux.skillengine.capability.SkillEngineCapabilities; +import net.fretux.skillengine.events.SkillPointsChangedEvent; import net.fretux.skillengine.skilltree.*; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.network.NetworkEvent; import net.minecraftforge.network.PacketDistributor; @@ -40,6 +42,14 @@ public class ServerboundUnlockNodePacket { if (unlockPlan != null) { for (SkillNode plannedNode : unlockPlan) { data.unlockNode(plannedNode); + if (plannedNode.getCost() > 0) { + MinecraftForge.EVENT_BUS.post(new SkillPointsChangedEvent( + player, + plannedNode.getCost(), + SkillPointsChangedEvent.Reason.SPENT, + plannedNode.getId() + )); + } } PacketHandler.syncSkillsTo(player); PacketHandler.CHANNEL.send( diff --git a/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java b/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java index 0eb35d8..b3c7656 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java +++ b/src/main/java/net/fretux/skillengine/skilltree/AbilityNode.java @@ -1,5 +1,6 @@ package net.fretux.skillengine.skilltree; +import net.fretux.skillengine.api.AbilityHandlerRegistry; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.ServerPlayer; @@ -48,6 +49,8 @@ public class AbilityNode { public int getCooldown() { return cooldown; } public void execute(ServerPlayer player) { - player.sendSystemMessage(Component.literal("Activated ability: " + id)); + if (!AbilityHandlerRegistry.execute(player, this)) { + player.sendSystemMessage(Component.literal("Activated ability: " + id)); + } } } diff --git a/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java index 2801ee3..b5084c2 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java +++ b/src/main/java/net/fretux/skillengine/skilltree/AbilityNodeRegistry.java @@ -8,16 +8,28 @@ import java.util.Map; public class AbilityNodeRegistry { private static final Map ABILITIES = new LinkedHashMap<>(); + private static final Map DUPLICATE_COUNTS = new LinkedHashMap<>(); + public static void clear() { ABILITIES.clear(); + DUPLICATE_COUNTS.clear(); } + public static void put(AbilityNode node) { - ABILITIES.put(node.getId(), node); + if (ABILITIES.put(node.getId(), node) != null) { + DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum); + } } + public static AbilityNode get(ResourceLocation id) { return ABILITIES.get(id); } + public static Collection all() { return ABILITIES.values(); } + + public static Map duplicateCounts() { + return Map.copyOf(DUPLICATE_COUNTS); + } } diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java index 17007a3..d7692d8 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java @@ -1,8 +1,9 @@ package net.fretux.skillengine.skilltree; -import net.fretux.ascend.player.PlayerStatsProvider; +import net.fretux.skillengine.api.PrerequisiteProviderRegistry; import net.fretux.skillengine.capability.PlayerSkillData; import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; import java.util.ArrayList; @@ -43,13 +44,13 @@ public class SkillLogic { public static boolean meetsAscendPrerequisites(Player player, SkillNode node) { if (node.getPrereqAttributes().isEmpty()) return true; - return player.getCapability(PlayerStatsProvider.PLAYER_STATS).map(stats -> { - for (Map.Entry req : node.getPrereqAttributes().entrySet()) { - int current = stats.getAttributeLevel(req.getKey()); - if (current < req.getValue()) return false; + if (!(player instanceof ServerPlayer serverPlayer)) return true; + for (Map.Entry req : node.getPrereqAttributes().entrySet()) { + if (!PrerequisiteProviderRegistry.meets(serverPlayer, node, req.getKey(), req.getValue())) { + return false; } - return true; - }).orElse(false); + } + return true; } private static boolean collectUnlockPlan(PlayerSkillData data, diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillNode.java b/src/main/java/net/fretux/skillengine/skilltree/SkillNode.java index d9c1fc7..de7039b 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillNode.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillNode.java @@ -16,6 +16,9 @@ public class SkillNode { private final float y; private final List links; private final List tags; + private final ResourceLocation tree; + private final String category; + private final String layer; private final ResourceLocation icons; private final Map prereqAttributes; private final List exclusiveWith; @@ -27,6 +30,9 @@ public class SkillNode { float x, float y, List links, List tags, + ResourceLocation tree, + String category, + String layer, ResourceLocation icons, Map prereqAttributes, List exclusiveWith){ @@ -39,6 +45,9 @@ public class SkillNode { this.y = y; this.links = List.copyOf(links); this.tags = List.copyOf(tags); + this.tree = tree; + this.category = category; + this.layer = layer; this.icons = icons; this.prereqAttributes = Map.copyOf(prereqAttributes); this.exclusiveWith = List.copyOf(exclusiveWith); @@ -76,6 +85,18 @@ public class SkillNode { return tags; } + public ResourceLocation getTree() { + return tree; + } + + public String getCategory() { + return category; + } + + public String getLayer() { + return layer; + } + public ResourceLocation getIcons() { return icons; } @@ -91,4 +112,4 @@ public class SkillNode { public boolean isRoot() { return links.isEmpty(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillNodeLoader.java b/src/main/java/net/fretux/skillengine/skilltree/SkillNodeLoader.java index 1369f5a..03145fe 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillNodeLoader.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillNodeLoader.java @@ -14,14 +14,18 @@ import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; @Mod.EventBusSubscriber(modid = SkillEngine.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE) public class SkillNodeLoader extends SimpleJsonResourceReloadListener { private static final Gson GSON = new GsonBuilder().create(); + private static final ResourceLocation DEFAULT_TREE = + ResourceLocation.fromNamespaceAndPath(SkillEngine.MODID, "main"); + private static final Pattern TRANSLATION_KEY = + Pattern.compile("[a-z0-9_.-]+\\.[a-z0-9_.-]+"); public SkillNodeLoader() { super(GSON, "skillnodes"); @@ -37,15 +41,14 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener { @NotNull ResourceManager resourceManager, @NotNull ProfilerFiller profiler) { SkillEngine.LOGGER.info("Loading skill nodes..."); - validateSingleSkillTreeProvider(jsons); SkillNodeRegistry.clear(); jsons.forEach((id, element) -> { JsonObject obj = element.getAsJsonObject(); ResourceLocation nodeId = obj.has("id") ? ResourceLocation.parse(obj.get("id").getAsString()) : id; - String title = obj.get("title").getAsString(); - String description = obj.get("description").getAsString(); + Component title = readComponent(obj, "title"); + Component description = readComponent(obj, "description"); int cost = obj.get("cost").getAsInt(); JsonObject pos = obj.getAsJsonObject("position"); float x = pos.get("x").getAsFloat(); @@ -54,8 +57,15 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener { obj.getAsJsonArray("links").forEach(e -> links.add(ResourceLocation.parse(e.getAsString()))); List tags = new ArrayList<>(); - obj.getAsJsonArray("tags").forEach(e -> - tags.add(ResourceLocation.parse(e.getAsString()))); + if (obj.has("tags")) { + obj.getAsJsonArray("tags").forEach(e -> + tags.add(ResourceLocation.parse(e.getAsString()))); + } + ResourceLocation tree = obj.has("tree") + ? ResourceLocation.parse(obj.get("tree").getAsString()) + : ResourceLocation.fromNamespaceAndPath(nodeId.getNamespace(), DEFAULT_TREE.getPath()); + String category = obj.has("category") ? obj.get("category").getAsString() : ""; + String layer = obj.has("layer") ? obj.get("layer").getAsString() : ""; ResourceLocation icons = ResourceLocation.parse(obj.get("icons").getAsString()); Map prereqAttributes = new HashMap<>(); if (obj.has("prerequisites")) { @@ -71,12 +81,15 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener { } SkillNode node = new SkillNode( nodeId, - Component.literal(title), - Component.literal(description), + title, + description, cost, x, y, links, tags, + tree, + category, + layer, icons, prereqAttributes, exclusiveWith @@ -87,28 +100,23 @@ public class SkillNodeLoader extends SimpleJsonResourceReloadListener { SkillEngine.LOGGER.info("Loaded {} skill nodes", SkillNodeRegistry.all().size()); } - private static void validateSingleSkillTreeProvider(Map jsons) { - Map> providers = new LinkedHashMap<>(); - jsons.keySet().stream() - .filter(id -> !SkillEngine.MODID.equals(id.getNamespace())) - .forEach(id -> providers.computeIfAbsent(id.getNamespace(), ignored -> new ArrayList<>()).add(id)); - - if (providers.size() <= 1) { - return; + private static Component readComponent(JsonObject obj, String key) { + if (!obj.has(key)) { + return Component.empty(); } - - StringBuilder message = new StringBuilder(); - message.append("Skill Engine detected multiple mods adding skill tree nodes: "); - providers.forEach((namespace, nodeIds) -> - message.append(namespace) - .append(" (") - .append(nodeIds.size()) - .append(" nodes), ")); - message.setLength(message.length() - 2); - message.append(". Skill Engine supports one skill tree provider at a time because multiple skill trees overlap in the same UI. Remove all but one skill tree addon."); - - String error = message.toString(); - SkillEngine.LOGGER.error(error); - throw new IllegalStateException(error); + JsonElement element = obj.get(key); + if (element.isJsonObject()) { + JsonObject text = element.getAsJsonObject(); + if (text.has("translate")) { + return Component.translatable(text.get("translate").getAsString()); + } + if (text.has("text")) { + return Component.literal(text.get("text").getAsString()); + } + } + String value = element.getAsString(); + return TRANSLATION_KEY.matcher(value).matches() + ? Component.translatable(value) + : Component.literal(value); } } diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillNodeRegistry.java b/src/main/java/net/fretux/skillengine/skilltree/SkillNodeRegistry.java index 81b5b33..9c49347 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillNodeRegistry.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillNodeRegistry.java @@ -3,19 +3,36 @@ package net.fretux.skillengine.skilltree; import net.minecraft.resources.ResourceLocation; import java.util.Collection; +import java.util.List; import java.util.LinkedHashMap; import java.util.Map; public class SkillNodeRegistry { private static final Map NODES = new LinkedHashMap<>(); + private static final Map> NODES_BY_TREE = new LinkedHashMap<>(); + private static final Map DUPLICATE_COUNTS = new LinkedHashMap<>(); public static void clear() { NODES.clear(); + NODES_BY_TREE.clear(); + DUPLICATE_COUNTS.clear(); } public static void put(SkillNode node) { - NODES.put(node.getId(), node); + SkillNode previous = NODES.put(node.getId(), node); + if (previous != null) { + DUPLICATE_COUNTS.merge(node.getId(), 1, Integer::sum); + Map previousTree = NODES_BY_TREE.get(previous.getTree()); + if (previousTree != null) { + previousTree.remove(previous.getId()); + if (previousTree.isEmpty()) { + NODES_BY_TREE.remove(previous.getTree()); + } + } + } + NODES_BY_TREE.computeIfAbsent(node.getTree(), ignored -> new LinkedHashMap<>()) + .put(node.getId(), node); } public static SkillNode get(ResourceLocation id) { @@ -25,4 +42,17 @@ public class SkillNodeRegistry { public static Collection all() { return NODES.values(); } + + public static List trees() { + return List.copyOf(NODES_BY_TREE.keySet()); + } + + public static Collection byTree(ResourceLocation tree) { + Map nodes = NODES_BY_TREE.get(tree); + return nodes != null ? nodes.values() : List.of(); + } + + public static Map duplicateCounts() { + return Map.copyOf(DUPLICATE_COUNTS); + } } diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java b/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java new file mode 100644 index 0000000..6a8984a --- /dev/null +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillTreeValidator.java @@ -0,0 +1,172 @@ +package net.fretux.skillengine.skilltree; + +import net.fretux.skillengine.api.PrerequisiteProviderRegistry; +import net.minecraft.resources.ResourceLocation; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public final class SkillTreeValidator { + + private SkillTreeValidator() {} + + public static List validate() { + List issues = new ArrayList<>(); + SkillNodeRegistry.duplicateCounts().forEach((id, count) -> + issues.add(id + " was loaded " + (count + 1) + " times; node ids must be unique")); + AbilityNodeRegistry.duplicateCounts().forEach((id, count) -> + issues.add(id + " was loaded " + (count + 1) + " times; ability ids must be unique")); + for (SkillNode node : SkillNodeRegistry.all()) { + validateNode(node, issues); + } + for (AbilityNode ability : AbilityNodeRegistry.all()) { + validateAbility(ability, issues); + } + detectSkillCycles(issues); + detectAbilityCycles(issues); + return issues; + } + + private static void validateNode(SkillNode node, List issues) { + if (!isPngTexture(node.getIcons())) { + issues.add(node.getId() + " has invalid icon path " + node.getIcons() + " (expected a .png texture)"); + } + for (ResourceLocation parent : node.getLinks()) { + if (SkillNodeRegistry.get(parent) == null) { + issues.add(node.getId() + " links to missing skill node " + parent); + } + } + for (ResourceLocation exclusive : node.getExclusiveWith()) { + SkillNode other = SkillNodeRegistry.get(exclusive); + if (other == null) { + issues.add(node.getId() + " is exclusive with missing node " + exclusive); + } else if (exclusive.equals(node.getId())) { + issues.add(node.getId() + " is exclusive with itself"); + } else if (!other.getExclusiveWith().contains(node.getId())) { + issues.add(node.getId() + " is exclusive with " + exclusive + " but the reverse entry is missing"); + } + } + Set requiredSkillNodes = collectRequiredSkillNodes(node); + for (ResourceLocation exclusive : node.getExclusiveWith()) { + if (requiredSkillNodes.contains(exclusive)) { + issues.add(node.getId() + " is impossible to unlock because required ancestor " + exclusive + " is mutually exclusive"); + } + } + List requiredList = new ArrayList<>(requiredSkillNodes); + for (int i = 0; i < requiredList.size(); i++) { + SkillNode first = SkillNodeRegistry.get(requiredList.get(i)); + if (first == null) continue; + for (int j = i + 1; j < requiredList.size(); j++) { + ResourceLocation second = requiredList.get(j); + if (first.getExclusiveWith().contains(second)) { + issues.add(node.getId() + " is impossible to unlock because required nodes " + + first.getId() + " and " + second + " are mutually exclusive"); + } + } + } + for (String key : node.getPrereqAttributes().keySet()) { + int separator = key.indexOf(':'); + if (separator >= 0 && (separator == 0 || separator == key.length() - 1)) { + issues.add(node.getId() + " has malformed prerequisite key " + key); + continue; + } + String namespace = separator > 0 ? key.substring(0, separator) : "ascend"; + if (!PrerequisiteProviderRegistry.hasProvider(namespace)) { + issues.add(node.getId() + " requires " + key + " but no prerequisite provider is registered for " + namespace); + } + } + } + + private static void validateAbility(AbilityNode ability, List issues) { + if (!isPngTexture(ability.getIcon())) { + issues.add(ability.getId() + " has invalid icon path " + ability.getIcon() + " (expected a .png texture)"); + } + for (ResourceLocation parent : ability.getLinks()) { + if (SkillNodeRegistry.get(parent) == null && AbilityNodeRegistry.get(parent) == null) { + issues.add(ability.getId() + " links to missing node or ability " + parent); + } + } + } + + private static boolean isPngTexture(ResourceLocation id) { + return id != null && id.getPath().endsWith(".png"); + } + + private static void detectSkillCycles(List issues) { + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); + for (SkillNode node : SkillNodeRegistry.all()) { + visit(node, visited, visiting, issues); + } + } + + private static void detectAbilityCycles(List issues) { + Set visited = new HashSet<>(); + Set visiting = new HashSet<>(); + for (AbilityNode ability : AbilityNodeRegistry.all()) { + visitAbility(ability, visited, visiting, issues); + } + } + + private static void visit(SkillNode node, + Set visited, + Set visiting, + List issues) { + ResourceLocation id = node.getId(); + if (visited.contains(id)) return; + if (!visiting.add(id)) { + issues.add("Cycle detected at skill node " + id); + return; + } + for (ResourceLocation parentId : node.getLinks()) { + SkillNode parent = SkillNodeRegistry.get(parentId); + if (parent != null) { + visit(parent, visited, visiting, issues); + } + } + visiting.remove(id); + visited.add(id); + } + + private static void visitAbility(AbilityNode ability, + Set visited, + Set visiting, + List issues) { + ResourceLocation id = ability.getId(); + if (visited.contains(id)) return; + if (!visiting.add(id)) { + issues.add("Cycle detected at ability node " + id); + return; + } + for (ResourceLocation parentId : ability.getLinks()) { + AbilityNode parent = AbilityNodeRegistry.get(parentId); + if (parent != null) { + visitAbility(parent, visited, visiting, issues); + } + } + visiting.remove(id); + visited.add(id); + } + + private static Set collectRequiredSkillNodes(SkillNode node) { + Set required = new HashSet<>(); + collectRequiredSkillNodes(node, required, new HashSet<>()); + required.remove(node.getId()); + return required; + } + + private static void collectRequiredSkillNodes(SkillNode node, + Set required, + Set visiting) { + if (!visiting.add(node.getId())) return; + for (ResourceLocation parentId : node.getLinks()) { + SkillNode parent = SkillNodeRegistry.get(parentId); + if (parent != null && required.add(parentId)) { + collectRequiredSkillNodes(parent, required, visiting); + } + } + visiting.remove(node.getId()); + } +} diff --git a/src/main/resources/assets/skillengine/lang/en_us.json b/src/main/resources/assets/skillengine/lang/en_us.json index fa982e4..49a52f0 100644 --- a/src/main/resources/assets/skillengine/lang/en_us.json +++ b/src/main/resources/assets/skillengine/lang/en_us.json @@ -3,6 +3,11 @@ "key.skillengine.openabilities": "Open Ability Screen", "key.skillengine.ability1": "Use Skill in first Skilltree Slot", "key.skillengine.ability2": "Use Skill in second Skilltree Slot", - "key.skillengine.ability3": "Use Skill in third Skilltree Slot" + "key.skillengine.ability3": "Use Skill in third Skilltree Slot", + "key.skillengine.ability4": "Use Skill in fourth Skilltree Slot", + "key.skillengine.ability5": "Use Skill in fifth Skilltree Slot", + "key.skillengine.ability6": "Use Skill in sixth Skilltree Slot", + "key.skillengine.ability7": "Use Skill in seventh Skilltree Slot", + "key.skillengine.ability8": "Use Skill in eighth Skilltree Slot", + "key.skillengine.ability9": "Use Skill in ninth Skilltree Slot" } -