From 3d54b3ffc169be96a978736e29e394fdc8b358cb Mon Sep 17 00:00:00 2001 From: fretuks Date: Mon, 18 May 2026 19:53:05 +0200 Subject: [PATCH] extend unlock logic to support multi-node unlock plans and update version number --- gradle.properties | 2 +- .../skillengine/api/SkillEngineAPI.java | 7 +- .../network/ServerboundUnlockNodePacket.java | 52 ++------- .../skillengine/skilltree/SkillLogic.java | 101 +++++++++++++++--- 4 files changed, 101 insertions(+), 61 deletions(-) diff --git a/gradle.properties b/gradle.properties index a5fb528..5a58832 100644 --- a/gradle.properties +++ b/gradle.properties @@ -48,7 +48,7 @@ mod_name=Fretux's Skill Engine # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=All Rights Reserved # The mod version. See https://semver.org/ -mod_version=1.3.1 +mod_version=1.3.2 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java index 332dc28..029df60 100644 --- a/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java +++ b/src/main/java/net/fretux/skillengine/api/SkillEngineAPI.java @@ -92,8 +92,11 @@ public final class SkillEngineAPI { if (data == null) return false; SkillNode node = SkillNodeRegistry.get(id); if (node == null) return false; - if (!SkillLogic.canUnlock(data, player, node)) return false; - data.unlockNode(node); + List unlockPlan = SkillLogic.getUnlockPlan(data, player, node).orElse(null); + if (unlockPlan == null) return false; + for (SkillNode plannedNode : unlockPlan) { + data.unlockNode(plannedNode); + } return true; } diff --git a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java index 215f81d..6482dde 100644 --- a/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java +++ b/src/main/java/net/fretux/skillengine/network/ServerboundUnlockNodePacket.java @@ -1,7 +1,5 @@ package net.fretux.skillengine.network; -import net.fretux.ascend.player.PlayerStats; -import net.fretux.ascend.player.PlayerStatsProvider; import net.fretux.skillengine.SkillEngine; import net.fretux.skillengine.capability.PlayerSkillData; import net.fretux.skillengine.capability.SkillEngineCapabilities; @@ -12,6 +10,7 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.network.NetworkEvent; import net.minecraftforge.network.PacketDistributor; +import java.util.List; import java.util.function.Supplier; public class ServerboundUnlockNodePacket { @@ -37,8 +36,11 @@ public class ServerboundUnlockNodePacket { AbilityNode ability = AbilityNodeRegistry.get(msg.nodeId); player.getCapability(SkillEngineCapabilities.PLAYER_SKILLS).ifPresent(data -> { if (node != null) { - if (canUnlockSkillNode(player, data, node)) { - data.unlockNode(node); + List unlockPlan = SkillLogic.getUnlockPlan(data, player, node).orElse(null); + if (unlockPlan != null) { + for (SkillNode plannedNode : unlockPlan) { + data.unlockNode(plannedNode); + } PacketHandler.syncSkillsTo(player); PacketHandler.CHANNEL.send( PacketDistributor.PLAYER.with(() -> player), @@ -66,46 +68,6 @@ public class ServerboundUnlockNodePacket { ctx.get().setPacketHandled(true); } - private static boolean canUnlockSkillNode(ServerPlayer player, PlayerSkillData data, SkillNode node) { - if (data.isUnlocked(node.getId())) { - SkillEngine.LOGGER.debug("cannot unlock {}: already unlocked", node.getId()); - return false; - } - if (data.getSkillPoints() < node.getCost()) { - SkillEngine.LOGGER.debug("cannot unlock {}: not enough points (have {}, need {})", - node.getId(), data.getSkillPoints(), node.getCost()); - return false; - } - for (ResourceLocation ex : node.getExclusiveWith()) { - if (data.isUnlocked(ex)) { - SkillEngine.LOGGER.debug("cannot unlock {}: mutually exclusive with {} which is already unlocked", - node.getId(), ex); - return false; - } - } - for (ResourceLocation parent : node.getLinks()) { - if (!data.isUnlocked(parent)) { - SkillEngine.LOGGER.debug("cannot unlock {}: parent {} is not unlocked", - node.getId(), parent); - return false; - } - } - PlayerStats stats = player.getCapability(PlayerStatsProvider.PLAYER_STATS).orElse(null); - if (stats == null) { - SkillEngine.LOGGER.debug("cannot unlock {}: no PlayerStats capability", node.getId()); - return false; - } - for (var e : node.getPrereqAttributes().entrySet()) { - int current = stats.getAttributeLevel(e.getKey()); - if (current < e.getValue()) { - SkillEngine.LOGGER.debug("cannot unlock {}: attribute {} is {}/{}", - node.getId(), e.getKey(), current, e.getValue()); - return false; - } - } - return true; - } - private static boolean canUnlockAbility(ServerPlayer player, PlayerSkillData data, AbilityNode ability) { if (data.isAbilityUnlocked(ability.getId())) return false; @@ -115,4 +77,4 @@ public class ServerboundUnlockNodePacket { } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java index 223e47c..17007a3 100644 --- a/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java +++ b/src/main/java/net/fretux/skillengine/skilltree/SkillLogic.java @@ -5,26 +5,44 @@ import net.fretux.skillengine.capability.PlayerSkillData; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Player; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; public class SkillLogic { public static boolean canUnlock(PlayerSkillData data, Player player, SkillNode node) { - if (data.isUnlocked(node.getId())) return false; - if (data.getSkillPoints() < node.getCost()) return false; - if (node.isRoot()) return true; - if (!meetsAscendPrerequisites(player, node)) return false; - for (ResourceLocation neighborId : node.getLinks()) { - if (data.isUnlocked(neighborId)) return true; - } - for (ResourceLocation excl : node.getExclusiveWith()) { - if (data.isUnlocked(excl)) { - return false; - } - } - return false; + return getUnlockPlan(data, player, node).isPresent(); } + + public static Optional> getUnlockPlan(PlayerSkillData data, Player player, SkillNode node) { + if (data == null || player == null || node == null || data.isUnlocked(node.getId())) { + return Optional.empty(); + } + + List plan = new ArrayList<>(); + Set planned = new HashSet<>(); + Set visiting = new HashSet<>(); + if (!collectUnlockPlan(data, player, node, plan, planned, visiting)) { + return Optional.empty(); + } + + int totalCost = 0; + for (SkillNode plannedNode : plan) { + totalCost += plannedNode.getCost(); + } + if (data.getSkillPoints() < totalCost) { + return Optional.empty(); + } + + return Optional.of(List.copyOf(plan)); + } + 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()); @@ -34,4 +52,61 @@ public class SkillLogic { }).orElse(false); } + private static boolean collectUnlockPlan(PlayerSkillData data, + Player player, + SkillNode node, + List plan, + Set planned, + Set visiting) { + ResourceLocation id = node.getId(); + if (data.isUnlocked(id) || planned.contains(id)) { + return true; + } + if (!visiting.add(id)) { + return false; + } + if (!meetsAscendPrerequisites(player, node) || conflictsWithUnlocked(data, node)) { + visiting.remove(id); + return false; + } + + for (ResourceLocation parentId : node.getLinks()) { + if (data.isUnlocked(parentId)) { + continue; + } + SkillNode parent = SkillNodeRegistry.get(parentId); + if (parent == null || !collectUnlockPlan(data, player, parent, plan, planned, visiting)) { + visiting.remove(id); + return false; + } + } + + if (conflictsWithPlanned(node, planned)) { + visiting.remove(id); + return false; + } + + visiting.remove(id); + planned.add(id); + plan.add(node); + return true; + } + + private static boolean conflictsWithUnlocked(PlayerSkillData data, SkillNode node) { + for (ResourceLocation excl : node.getExclusiveWith()) { + if (data.isUnlocked(excl)) { + return true; + } + } + return false; + } + + private static boolean conflictsWithPlanned(SkillNode node, Set planned) { + for (ResourceLocation excl : node.getExclusiveWith()) { + if (planned.contains(excl)) { + return true; + } + } + return false; + } }