From 5225e970424aa64a8e60fed5b12dc5ab872f3d28 Mon Sep 17 00:00:00 2001 From: Jules Date: Mon, 13 Oct 2025 23:42:55 +0200 Subject: [PATCH] Skill trees now cache node&path shapes&states to reduce impact on performance --- .../Indyuce/mmocore/api/ConfigMessage.java | 3 +- .../mmocore/api/player/PlayerData.java | 216 ++++++++----- .../player/profess/SavedClassInformation.java | 2 +- .../gui/skilltree/SkillTreeViewer.java | 70 +++-- .../mmocore/manager/SkillTreeManager.java | 9 +- .../mmocore/skill/cast/handler/KeyCombos.java | 7 +- .../skill/cast/handler/SkillScroller.java | 6 +- ...IntegerCoordinates.java => IntCoords.java} | 23 +- .../mmocore/skilltree/ParentInformation.java | 161 +++++++++- .../mmocore/skilltree/SkillTreeNode.java | 132 ++++---- .../mmocore/skilltree/SkillTreePath.java | 67 ---- .../mmocore/skilltree/display/DisplayMap.java | 1 - .../skilltree/display/PathDisplayInfo.java | 3 +- .../skilltree/{ => display}/PathState.java | 7 +- .../skilltree/tree/ProximitySkillTree.java | 21 +- .../mmocore/skilltree/tree/SkillTree.java | 288 +++++++++++------- 16 files changed, 631 insertions(+), 385 deletions(-) rename MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/{IntegerCoordinates.java => IntCoords.java} (74%) delete mode 100644 MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreePath.java rename MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/{ => display}/PathState.java (80%) diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/ConfigMessage.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/ConfigMessage.java index 76825183..6d3fd02d 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/ConfigMessage.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/ConfigMessage.java @@ -1,6 +1,7 @@ package net.Indyuce.mmocore.api; import io.lumine.mythic.lib.MythicLib; +import io.lumine.mythic.lib.message.actionbar.ActionBarPriority; import io.lumine.mythic.lib.util.lang3.Validate; import net.Indyuce.mmocore.MMOCore; import net.Indyuce.mmocore.api.player.PlayerData; @@ -162,7 +163,7 @@ public class ConfigMessage { // Handle special case with player data + action bar if (playerData != null && playerData.isOnline() && actionbar) { - playerData.displayActionBar(rawMessage, raw); + playerData.getMMOPlayerData().getActionBar().show(ActionBarPriority.NORMAL, rawMessage); return; } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java index c784eea6..3c13980b 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/PlayerData.java @@ -51,8 +51,8 @@ import net.Indyuce.mmocore.skill.binding.BoundSkillInfo; import net.Indyuce.mmocore.skill.binding.SkillSlot; import net.Indyuce.mmocore.skill.cast.SkillCastingInstance; import net.Indyuce.mmocore.skill.cast.SkillCastingMode; -import net.Indyuce.mmocore.skilltree.NodeState; -import net.Indyuce.mmocore.skilltree.SkillTreeNode; +import net.Indyuce.mmocore.skilltree.*; +import net.Indyuce.mmocore.skilltree.display.PathState; import net.Indyuce.mmocore.skilltree.tree.SkillTree; import net.Indyuce.mmocore.util.Language; import net.Indyuce.mmocore.waypoint.Waypoint; @@ -112,25 +112,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD private final Map lastActivity = new HashMap<>(); private final CombatHandler combat = new CombatHandler(this); - /** - * Cached data - *

- * Current state of each node. This does not get saved in the player database - * as it can be inferred from the skill tree node levels map. - */ - private final Map nodeStates = new HashMap<>(); - - private final Map nodeLevels = new HashMap<>(); - private final Map skillTreePoints = new HashMap<>(); - - /** - * Cached data - *

- * Amount of points spent in each tree. This does not get saved in the - * player database as it can be inferred from the skill tree node levels map. - */ - private final Map skillTreePointsSpent = new HashMap<>(); - /** * Saves the NSK's of the items that have been unlocked in the format "namespace:key". * This is used for: @@ -228,13 +209,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD node.getExperienceTable().applyTemporaryTriggers(this, node); } - private void setupSkillTrees() { - - // Node states setup - for (SkillTree skillTree : getProfess().getSkillTrees()) - skillTree.setupNodeStates(this); - } - @Override protected void onSessionReady() { @@ -268,6 +242,58 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD setStellium(lastStellium, PlayerResourceUpdateEvent.UpdateReason.CHOOSE_CLASS); } + @Override + public Map mapBoundSkills() { + Map result = new HashMap<>(); + for (int slot : boundSkills.keySet()) + result.put(slot, boundSkills.get(slot).getClassSkill().getSkill().getHandler().getId()); + return result; + } + + public void resetTriggerStats() { + getMMOPlayerData().getStatMap().getInstances().forEach(statInstance -> statInstance.removeIf(Trigger.STAT_MODIFIER_KEY::equals)); + getMMOPlayerData().getSkillModifierMap().removeModifiers(Trigger.STAT_MODIFIER_KEY); + } + + //region Skill trees + + /** + * Current state of each node. This does not get saved in the player database + * as it can be inferred from the skill tree node levels map. This is used + * mainly for displaying the skill tree in the GUI. + */ + private final Map nodeStates = new HashMap<>(); + + /** + * Current state of each path. This does not get saved in the player database + * as it can be inferred from the skill tree node levels map. This is used + * mainly for displaying the skill tree in the GUI. + */ + private final Map edgeStates = new HashMap<>(); + + private final Map nodeLevels = new HashMap<>(); + private final Map skillTreePoints = new HashMap<>(); + + /** + * Cached data + *

+ * Amount of points spent in each tree. This does not get saved in the + * player database as it can be inferred from the skill tree node levels map. + */ + private final Map skillTreePointsSpent = new HashMap<>(); + + @Override + public int getSkillTreeReallocationPoints() { + return skillTreeReallocationPoints; + } + + private void setupSkillTrees() { + + // Node states setup + for (SkillTree skillTree : getProfess().getSkillTrees()) + skillTree.resolveStates(this); + } + public int getPointsSpent(@NotNull SkillTree skillTree) { return skillTreePointsSpent.getOrDefault(skillTree, 0); } @@ -276,11 +302,11 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD private static String asKey(@Nullable SkillTree skillTree) { return skillTree == null ? Arguments.SKILL_TREE_GLOBAL_KEY : skillTree.getId(); } - + public void setSkillTreePoints(@NotNull SkillTree skillTree, int points) { setSkillTreePoints(asKey(skillTree), points); } - + public void setSkillTreePoints(@NotNull String skillTreeId, int points) { if (points <= 0) skillTreePoints.remove(skillTreeId); else skillTreePoints.put(skillTreeId, points); @@ -294,15 +320,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD skillTreePoints.merge(id, Math.max(0, val), (points, ignored) -> Math.max(0, points + val)); } - /** - * Counts the number of points spent in that skill tree - * @deprecated - * @see #getPointsSpent(SkillTree) - */ - public int countSkillTreePoints(@NotNull SkillTree skillTree) { - return nodeLevels.keySet().stream().filter(node -> node.getTree().equals(skillTree)).mapToInt(node -> nodeLevels.get(node) * node.getPointConsumption()).sum(); - } - /** * Make a copy to make sure that the object * created is independent of the state of playerData. @@ -312,14 +329,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return new HashMap<>(skillTreePoints); } - @Override - public Map mapBoundSkills() { - Map result = new HashMap<>(); - for (int slot : boundSkills.keySet()) - result.put(slot, boundSkills.get(slot).getClassSkill().getSkill().getHandler().getId()); - return result; - } - public Set> getNodeLevelsEntrySet() { HashMap nodeLevelsString = new HashMap<>(); for (SkillTreeNode node : nodeLevels.keySet()) @@ -327,11 +336,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return nodeLevelsString.entrySet(); } - public void resetTriggerStats() { - getMMOPlayerData().getStatMap().getInstances().forEach(statInstance -> statInstance.removeIf(Trigger.STAT_MODIFIER_KEY::equals)); - getMMOPlayerData().getSkillModifierMap().removeModifiers(Trigger.STAT_MODIFIER_KEY); - } - @Override public Map getNodeLevels() { final Map mapped = new HashMap<>(); @@ -349,6 +353,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD // Skill trees progress nodeLevels.clear(); nodeStates.clear(); // Cache data + edgeStates.clear(); skillTreePointsSpent.clear(); tableItemClaims.keySet().removeIf(s -> s.startsWith(SkillTreeNode.KEY_PREFIX)); // Clear node claim count @@ -360,13 +365,14 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD setupSkillTrees(); } - public void clearNodeStates(@NotNull SkillTree skillTree) { - for (SkillTreeNode node : skillTree.getNodes()) nodeStates.remove(node); + public void clearStates(@NotNull SkillTree skillTree) { + for (var node : skillTree.getNodes()) nodeStates.remove(node); + // TODO clear path states. } - // TODO move to UI...? @NotNull public NodeIncrementResult canIncrementNodeLevel(@NotNull SkillTreeNode node) { + // TODO move to UI...? final var nodeState = nodeStates.get(node); // Node is maxed out @@ -395,10 +401,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD final int newLevel = addNodeLevels(node, 1); node.updateAdvancement(this, newLevel); // Claim the node exp table - // Update node state - // TODO check/remove: USELESS LINE? since node states are updated afterwards - // nodeStates.compute(node, (key, status) -> status == NodeState.UNLOCKABLE ? NodeState.UNLOCKED : status); - // Consume skill tree points final AtomicInteger cost = new AtomicInteger(node.getPointConsumption()); skillTreePoints.computeIfPresent(node.getTree().getId(), (key, points) -> { @@ -408,9 +410,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD }); if (cost.get() > 0) withdrawSkillTreePoints("global", cost.get()); - // Reload node states from full skill tree - clearNodeStates(node.getTree()); - node.getTree().setupNodeStates(this); + // Reload node/path states from full skill tree + node.getTree().resolveStates(this); } public int getSkillTreePoints(@Nullable SkillTree skillTree) { @@ -426,14 +427,22 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD skillTreePoints.computeIfPresent(treeId, (ignored, points) -> cost >= points ? null : points - cost); } - public void setNodeState(SkillTreeNode node, NodeState nodeState) { + public void setNodeState(@NotNull SkillTreeNode node, @NotNull NodeState nodeState) { nodeStates.put(node, nodeState); } - public NodeState getNodeState(SkillTreeNode node) { + public void setPathState(@NotNull ParentInformation edge, @NotNull PathState pathState) { + edgeStates.put(edge, pathState); + } + + public NodeState getNodeState(@NotNull SkillTreeNode node) { return nodeStates.get(node); } + public PathState getPathState(@NotNull ParentInformation path) { + return edgeStates.get(path); + } + public boolean hasNodeState(@NotNull SkillTreeNode node) { return nodeStates.containsKey(node); } @@ -461,9 +470,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD for (SkillTreeNode node : skillTree.getNodes()) { node.resetAdvancement(this, true); setNodeLevel(node, 0); - nodeStates.remove(node); } - skillTree.setupNodeStates(this); + skillTree.resolveStates(this); } @NotNull @@ -475,6 +483,8 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return !nodeStates.isEmpty(); } + //endregion Skill trees + @Override public Map getNodeTimesClaimed() { Map result = new HashMap<>(); @@ -650,11 +660,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return attributeReallocationPoints; } - @Override - public int getSkillTreeReallocationPoints() { - return skillTreeReallocationPoints; - } - public int getClaims(@NotNull ExperienceObject object, @NotNull ExperienceItem item) { final ExperienceTable table = object.getExperienceTable(); return getClaims(object.getKey() + "." + table.getId() + "." + item.getId()); @@ -698,11 +703,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return guild != null; } - @Deprecated - public void setLevel(int level) { - setLevel(level, PlayerLevelChangeEvent.Reason.UNKNOWN); - } - public void setLevel(int level, @NotNull PlayerLevelChangeEvent.Reason reason) { final var oldLevel = this.level; @@ -718,11 +718,6 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD } } - @Deprecated - public void takeLevels(int value) { - setLevel(level - value, PlayerLevelChangeEvent.Reason.UNKNOWN); - } - public void giveLevels(int value, @NotNull EXPSource source) { long equivalentExp = 0; while (value-- > 0) equivalentExp += getProfess().getExpCurve().getExperience(getLevel() + value + 1); @@ -1398,16 +1393,43 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD //region Deprecated + @Deprecated + public void takeLevels(int value) { + setLevel(level - value, PlayerLevelChangeEvent.Reason.UNKNOWN); + } + + @Deprecated + public void setLevel(int level) { + setLevel(level, PlayerLevelChangeEvent.Reason.UNKNOWN); + } + + @Deprecated + public void clearNodeStates(@NotNull SkillTree skillTree) { + clearStates(skillTree); + } + + /** + * @see #setMana(double, PlayerResourceUpdateEvent.UpdateReason) + * @deprecated + */ @Deprecated public void setMana(double amount) { setMana(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN); } + /** + * @see #setStamina(double, PlayerResourceUpdateEvent.UpdateReason) + * @deprecated + */ @Deprecated public void setStamina(double amount) { setStamina(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN); } + /** + * @see #setStellium(double, PlayerResourceUpdateEvent.UpdateReason) + * @deprecated + */ @Deprecated public void setStellium(double amount) { setStellium(amount, PlayerResourceUpdateEvent.UpdateReason.UNKNOWN); @@ -1539,11 +1561,19 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD return false; } + /** + * @see #hasUnlockedLevel(ClassSkill) + * @deprecated + */ @Deprecated public boolean hasSkillUnlocked(RegisteredSkill skill) { return getProfess().hasSkill(skill.getHandler().getId()) && hasSkillUnlocked(getProfess().getSkill(skill.getHandler().getId())); } + /** + * @see #hasUnlockedLevel(ClassSkill) + * @deprecated + */ @Deprecated public boolean hasSkillUnlocked(ClassSkill skill) { return hasUnlockedLevel(skill); @@ -1552,6 +1582,9 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD /** * Everytime a player does a combat action, like taking * or dealing damage to an entity, this method is called. + * + * @see #getCombat() + * @deprecated */ @Deprecated public void updateCombat() { @@ -1560,7 +1593,7 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD @Deprecated public void displayActionBar(@NotNull String message) { - displayActionBar(message, false); + getMMOPlayerData().getActionBar().show(ActionBarPriority.NORMAL, message); } @Deprecated @@ -1580,15 +1613,34 @@ public class PlayerData extends SynchronizedDataHolder implements OfflinePlayerD } } + /** + * @see #getAttributes() + * @deprecated + */ @Deprecated public void setAttribute(PlayerAttribute attribute, int value) { setAttribute(attribute.getId(), value); } + /** + * @see #getAttributes() + * @deprecated + */ @Deprecated public void setAttribute(String id, int value) { attributes.getInstance(id).setBase(value); } + /** + * Counts the number of points spent in that skill tree + * + * @see #getPointsSpent(SkillTree) + * @deprecated + */ + @Deprecated + public int countSkillTreePoints(@NotNull SkillTree skillTree) { + return nodeLevels.keySet().stream().filter(node -> node.getTree().equals(skillTree)).mapToInt(node -> nodeLevels.get(node) * node.getPointConsumption()).sum(); + } + //endregion } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/profess/SavedClassInformation.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/profess/SavedClassInformation.java index aec440a1..3dd7ce61 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/profess/SavedClassInformation.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/api/player/profess/SavedClassInformation.java @@ -332,7 +332,7 @@ public class SavedClassInformation implements ClassDataContainer { for (SkillTreeNode node : skillTree.getNodes()) player.setNodeLevel(node, nodeLevels.getOrDefault(node.getFullId(), 0)); - skillTree.setupNodeStates(player); + skillTree.resolveStates(player); } // Add the values to the times claimed table and claims the corresponding stat triggers. diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/gui/skilltree/SkillTreeViewer.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/gui/skilltree/SkillTreeViewer.java index 8f4b1d4a..b4a0aed2 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/gui/skilltree/SkillTreeViewer.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/gui/skilltree/SkillTreeViewer.java @@ -13,7 +13,7 @@ import io.lumine.mythic.lib.gui.util.IconOptions; import net.Indyuce.mmocore.MMOCore; import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.player.Message; -import net.Indyuce.mmocore.skilltree.IntegerCoordinates; +import net.Indyuce.mmocore.skilltree.IntCoords; import net.Indyuce.mmocore.skilltree.NodeState; import net.Indyuce.mmocore.skilltree.ParentType; import net.Indyuce.mmocore.skilltree.SkillTreeNode; @@ -22,7 +22,6 @@ import net.Indyuce.mmocore.skilltree.display.NodeDisplayInfo; import net.Indyuce.mmocore.skilltree.display.PathDisplayInfo; import net.Indyuce.mmocore.skilltree.tree.SkillTree; import org.bukkit.ChatColor; -import org.bukkit.Material; import org.bukkit.NamespacedKey; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.event.inventory.ClickType; @@ -127,11 +126,10 @@ public class SkillTreeViewer extends EditableInventory { } int reallocated = inv.playerData.getPointsSpent(inv.skillTree); - // Remove all the nodeStates progress inv.playerData.giveSkillTreePoints(inv.skillTree.getId(), reallocated); inv.playerData.giveSkillTreeReallocationPoints(-1); inv.playerData.resetSkillTree(inv.skillTree); - inv.skillTree.setupNodeStates(inv.playerData); + inv.skillTree.resolveStates(inv.playerData); Message.SKILL_TREE_REALLOCATE.send(inv.playerData, "points", inv.playerData.getSkillTreePoints(inv.skillTree.getId()), "skill-tree", inv.skillTree.getName()); inv.open(); } @@ -304,7 +302,7 @@ public class SkillTreeViewer extends EditableInventory { @Override public void preprocessLore(@NotNull SkillTreeInventory inv, int n, @NotNull List lore) { - IntegerCoordinates coordinates = inv.getCoordinates(n); + IntCoords coordinates = inv.getCoordinates(n); if (!inv.getSkillTree().isNode(coordinates)) return; SkillTreeNode node = inv.getSkillTree().getNode(coordinates); @@ -346,10 +344,11 @@ public class SkillTreeViewer extends EditableInventory { */ @Override public ItemStack getDisplayedItem(SkillTreeInventory inv, int n) { - IntegerCoordinates coordinates = inv.getCoordinates(n); - if (!inv.getSkillTree().isPathOrNode(coordinates)) return new ItemStack(Material.AIR); + IntCoords coordinates = inv.getCoordinates(n); + + IconOptions icon = inv.computeIcon(coordinates); + if (icon == null) return null; // Neither a path nor a node - IconOptions icon = inv.getIcon(coordinates); ItemStack item = super.getDisplayedItem(inv, new ItemOptions(n, icon)); ItemMeta meta = item.getItemMeta(); @@ -377,6 +376,7 @@ public class SkillTreeViewer extends EditableInventory { * Soft&Strong children lore for the node */ public List getParentsLore(SkillTreeInventory inv, SkillTreeNode node, Collection parents) { + // TODO why is this hardcoded >:( List lore = new ArrayList<>(); for (SkillTreeNode parent : parents) { int level = inv.playerData.getNodeLevel(parent); @@ -387,7 +387,7 @@ public class SkillTreeViewer extends EditableInventory { } @Override - public Placeholders getPlaceholders(SkillTreeInventory inv, int n) { + public @NotNull Placeholders getPlaceholders(SkillTreeInventory inv, int n) { Placeholders holders = new Placeholders(); holders.register("skill-tree", inv.getSkillTree().getName()); boolean isNode = inv.getSkillTree().isNode(inv.getCoordinates(n)); @@ -400,10 +400,10 @@ public class SkillTreeViewer extends EditableInventory { holders.register("name", node.getName()); holders.register("max-children", node.getMaxChildren()); holders.register("point-consumed", node.getPointConsumption()); - holders.register("display-type", node.getNodeType()); - } else { + //holders.register("display-type", node.getNodeType()); + } /*else { holders.register("display-type", inv.skillTree.getPath(inv.getCoordinates(n)).getPathType()); - } + }*/ int maxPointSpent = inv.getSkillTree().getMaxPointSpent(); holders.register("max-point-spent", maxPointSpent == Integer.MAX_VALUE ? "∞" : maxPointSpent); holders.register("point-spent", inv.playerData.getPointsSpent(inv.getSkillTree())); @@ -420,10 +420,10 @@ public class SkillTreeViewer extends EditableInventory { final PersistentDataContainer container = event.getCurrentItem().getItemMeta().getPersistentDataContainer(); final int x = container.get(new NamespacedKey(MMOCore.plugin, "coordinates.x"), PersistentDataType.INTEGER); final int y = container.get(new NamespacedKey(MMOCore.plugin, "coordinates.y"), PersistentDataType.INTEGER); - if (!inv.skillTree.isNode(new IntegerCoordinates(x, y))) return; + if (!inv.skillTree.isNode(new IntCoords(x, y))) return; // Higher number of points spent in SKILL TREE (not node) - final SkillTreeNode node = inv.skillTree.getNode(new IntegerCoordinates(x, y)); + final SkillTreeNode node = inv.skillTree.getNode(new IntCoords(x, y)); if (inv.playerData.getPointsSpent(inv.skillTree) >= inv.skillTree.getMaxPointSpent()) { Message.SKILL_TREE_MAX_POINTS_SPENT.send(inv.playerData); return; @@ -518,24 +518,39 @@ public class SkillTreeViewer extends EditableInventory { return playerData; } - public IconOptions getIcon(@NotNull IntegerCoordinates coordinates) { - if (skillTree.isNode(coordinates)) { - var node = skillTree.getNode(coordinates); - var nodeShape = node.getNodeType(); - var nodeState = playerData.getNodeState(node); + @Deprecated + public IconOptions getIcon(IntCoords coordinates) { + return computeIcon(coordinates); + } + + @Nullable + public IconOptions computeIcon(@NotNull IntCoords coordinates) { + + // Is this a node? + final var node = skillTree.getNodeOrNull(coordinates); + if (node != null) { + final var nodeShape = skillTree.getNodeShape(node); + final var nodeState = playerData.getNodeState(node); var displayInfo = new NodeDisplayInfo(nodeShape, nodeState); // Node > skill tree > skill tree UI var icon = DisplayMap.getIcon(displayInfo, node.getIcons(), skillTree.getIcons(), icons); + if (icon == null && nodeState == NodeState.MAXED_OUT) { + // Fallback to UNLOCKED if no MAXED_OUT icon found + displayInfo = new NodeDisplayInfo(nodeShape, NodeState.UNLOCKED); + icon = DisplayMap.getIcon(displayInfo, node.getIcons(), skillTree.getIcons(), icons); + } if (icon == null) icon = DisplayMap.DEFAULT_ICON; //Validate.notNull(icon, "Node " + node.getFullId() + " has no icon for shape " + nodeShape + " and state " + nodeState); return icon; - } else { - var path = skillTree.getPath(coordinates); - var pathShape = path.getPathType(); - var pathStatus = path.getStatus(playerData); - var displayInfo = new PathDisplayInfo(pathShape, pathStatus); + } + + final var edge = skillTree.getPath(coordinates); + if (edge != null) { + final var pathState = playerData.getPathState(edge); + final var pathShape = edge.getShape(coordinates); + final var displayInfo = new PathDisplayInfo(pathShape, pathState); // Skill tree > Skill tree UI var icon = DisplayMap.getIcon(displayInfo, skillTree.getIcons(), icons); @@ -544,6 +559,9 @@ public class SkillTreeViewer extends EditableInventory { return icon; } + + // Neither a node or a path + return null; } @NotNull @@ -552,11 +570,11 @@ public class SkillTreeViewer extends EditableInventory { return guiName.replace("{skill-tree-name}", skillTree.getName()).replace("{skill-tree-id}", skillTree.getId()); } - public IntegerCoordinates getCoordinates(int n) { + public IntCoords getCoordinates(int n) { int slot = slots.get(n); int deltaX = (slot - minSlot) % 9; int deltaY = (slot - minSlot) / 9; - IntegerCoordinates coordinates = new IntegerCoordinates(getX() + deltaX, getY() + deltaY); + IntCoords coordinates = new IntCoords(getX() + deltaX, getY() + deltaY); return coordinates; } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/manager/SkillTreeManager.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/manager/SkillTreeManager.java index 048819c2..c8fa6129 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/manager/SkillTreeManager.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/manager/SkillTreeManager.java @@ -55,14 +55,7 @@ public class SkillTreeManager extends MMOCoreRegister { @NotNull public SkillTree loadSkillTree(@NotNull ConfigurationSection config) { Validate.notNull(config, "Config cannot be null"); - - final SkillTreeType type; - try { - type = SkillTreeType.valueOf(UtilityMethods.enumName(config.getString("type", "custom"))); - } catch (RuntimeException exception) { - throw new IllegalArgumentException("Not a valid skill tree type"); - } - + final var type = UtilityMethods.prettyValueOf(SkillTreeType::valueOf, config.getString("type", "custom"), "No skill tree type '%s'"); return type.construct(config); } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/KeyCombos.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/KeyCombos.java index 182ad350..82fb826e 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/KeyCombos.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/KeyCombos.java @@ -3,6 +3,7 @@ package net.Indyuce.mmocore.skill.cast.handler; import io.lumine.mythic.lib.api.event.skill.PlayerCastSkillEvent; import io.lumine.mythic.lib.api.player.EquipmentSlot; import io.lumine.mythic.lib.gui.editable.placeholder.Placeholders; +import io.lumine.mythic.lib.message.actionbar.ActionBarPriority; import io.lumine.mythic.lib.player.PlayerMetadata; import io.lumine.mythic.lib.skill.result.SkillResult; import io.lumine.mythic.lib.skill.trigger.TriggerMetadata; @@ -195,7 +196,11 @@ public class KeyCombos extends SkillCastingHandler { public void onTick() { if (actionBarOptions != null) if (actionBarOptions.isSubtitle) getCaster().getPlayer().sendTitle(" ", actionBarOptions.format(this), 0, 20, 0); - else getCaster().displayActionBar(actionBarOptions.format(this)); + else { + var handler = caster.getMMOPlayerData().getActionBar(); + if (!handler.canShow(ActionBarPriority.NORMAL)) return; + handler.show(ActionBarPriority.NORMAL, actionBarOptions.format(this)); + } } /** diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/SkillScroller.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/SkillScroller.java index 6335c69c..f9469f79 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/SkillScroller.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skill/cast/handler/SkillScroller.java @@ -3,6 +3,7 @@ package net.Indyuce.mmocore.skill.cast.handler; import io.lumine.mythic.lib.MythicLib; import io.lumine.mythic.lib.UtilityMethods; import io.lumine.mythic.lib.api.player.EquipmentSlot; +import io.lumine.mythic.lib.message.actionbar.ActionBarPriority; import io.lumine.mythic.lib.skill.trigger.TriggerMetadata; import io.lumine.mythic.lib.util.SoundObject; import net.Indyuce.mmocore.MMOCore; @@ -127,7 +128,10 @@ public class SkillScroller extends SkillCastingHandler { public void onTick() { final String skillName = getSelected().getSkill().getName(); final String actionBarFormat = MythicLib.plugin.getPlaceholderParser().parse(getCaster().getPlayer(), SkillScroller.this.actionBarFormat.replace("{selected}", skillName)); - getCaster().displayActionBar(actionBarFormat); + + var handler = caster.getMMOPlayerData().getActionBar(); + if (!handler.canShow(ActionBarPriority.NORMAL)) return; + handler.show(ActionBarPriority.NORMAL, actionBarFormat); } public ClassSkill getSelected() { diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntegerCoordinates.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntCoords.java similarity index 74% rename from MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntegerCoordinates.java rename to MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntCoords.java index b7b917e1..6c510929 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntegerCoordinates.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/IntCoords.java @@ -8,22 +8,27 @@ import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Objects; -public class IntegerCoordinates { +public class IntCoords { private final int x, y; - public IntegerCoordinates(int x, int y) { + public IntCoords(int x, int y) { this.x = x; this.y = y; } @Deprecated - public IntegerCoordinates(String str) { + public IntCoords(String str) { String[] split = str.split("\\."); Validate.isTrue(split.length == 2, "Invalid format"); x = Integer.parseInt(split[0]); y = Integer.parseInt(split[1]); } + @NotNull + public IntCoords offset(int x, int y) { + return new IntCoords(this.x + x, this.y + y); + } + public int getX() { return x; } @@ -33,15 +38,15 @@ public class IntegerCoordinates { } @NotNull - public IntegerCoordinates add(@NotNull IntegerCoordinates other) { - return new IntegerCoordinates(x + other.x, y + other.y); + public IntCoords add(@NotNull IntCoords other) { + return new IntCoords(x + other.x, y + other.y); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - IntegerCoordinates that = (IntegerCoordinates) o; + IntCoords that = (IntCoords) o; return x == that.x && y == that.y; } @@ -56,18 +61,18 @@ public class IntegerCoordinates { } @NotNull - public static IntegerCoordinates from(@Nullable Object object) { + public static IntCoords from(@Nullable Object object) { Validate.notNull(object, "Could not read coordinates"); if (object instanceof ConfigurationSection) { final ConfigurationSection config = (ConfigurationSection) object; - return new IntegerCoordinates(config.getInt("x"), config.getInt("y")); + return new IntCoords(config.getInt("x"), config.getInt("y")); } if (object instanceof String) { final String[] split = ((String) object).split("[:.,]"); Validate.isTrue(split.length > 1, "Must provide two coordinates, X and Y, got " + Arrays.asList(split)); - return new IntegerCoordinates(Integer.parseInt(split[0]), Integer.parseInt(split[1])); + return new IntCoords(Integer.parseInt(split[0]), Integer.parseInt(split[1])); } throw new RuntimeException("Needs either a string or configuration section"); diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/ParentInformation.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/ParentInformation.java index de94a529..edd67094 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/ParentInformation.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/ParentInformation.java @@ -1,23 +1,138 @@ package net.Indyuce.mmocore.skilltree; +import net.Indyuce.mmocore.skilltree.display.PathShape; +import org.apache.commons.lang3.Validate; +import org.bukkit.configuration.ConfigurationSection; import org.jetbrains.annotations.NotNull; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; +import java.util.Set; +/** + * Holds information about a parent/child node in a skill tree. + * `relative` can hold either a parent or child node. There is + * always one `parent` counterpart for every `child` instance of this class. + *

+ * If we represent the skill tree by a graph where edges are skill tree + * "nodes", then this class represents an edge in that graph. + * + * @author jules + */ public class ParentInformation { - private final SkillTreeNode node; + private final SkillTreeNode child, parent; private final ParentType type; - private final int level; + private final int minLevel; + private final boolean reciprocal; - public ParentInformation(SkillTreeNode node, ParentType type, int level) { - this.node = node; + private final Map elements = new HashMap<>(); + + public ParentInformation(@NotNull SkillTreeNode child, @NotNull SkillTreeNode parent) { + this(child, parent, ParentType.SOFT, false, 1); + } + + public ParentInformation(@NotNull SkillTreeNode child, + @NotNull SkillTreeNode parent, + @NotNull ParentType type, + boolean reciprocal, + int minLevel) { + this.child = child; + this.parent = parent; this.type = type; - this.level = level; + this.reciprocal = reciprocal; + this.minLevel = Math.max(1, minLevel); + } + + public ParentInformation(@NotNull SkillTreeNode child, + @NotNull SkillTreeNode parent, + @NotNull ParentType type, + @NotNull ConfigurationSection config) { + this.child = child; + this.parent = parent; + this.type = type; + this.reciprocal = false; + this.minLevel = Math.max(1, config.getInt("level")); + + // Read paths + if (config.contains("paths")) { + final var pathListRaw = config.getStringList("paths"); + pathListRaw.forEach(string -> this.elements.put(IntCoords.from(string), null)); + } + + // All paths are loaded => cache their shapes + for (var element : this.elements.keySet()) { + final var previousValue = this.elements.put(element, computePathShape(element)); + Validate.isTrue(previousValue == null, "Path shape already computed?"); + } } @NotNull - public SkillTreeNode getNode() { - return node; + public PathShape getShape(@NotNull IntCoords coordinates) { + final var shape = this.elements.get(coordinates); + Validate.notNull(shape, "No path element at " + coordinates); + return shape; + } + + /** + * Defines the method for computing the shape of a path element ie + * whether it goes up, right, up-right, etc. based on the presence + * of other path elements around it. + * + * @param coordinates Coordinates of the path element to compute the shape for + * @return Shape of the path element + */ + @NotNull + private PathShape computePathShape(@NotNull IntCoords coordinates) { + + final var upCoords = new IntCoords(coordinates.getX(), coordinates.getY() - 1); + final var downCoords = new IntCoords(coordinates.getX(), coordinates.getY() + 1); + final var rightCoords = new IntCoords(coordinates.getX() + 1, coordinates.getY()); + final var leftCoords = new IntCoords(coordinates.getX() - 1, coordinates.getY()); + + final var hasUp = this.elements.containsKey(upCoords) || upCoords.equals(parent.getCoordinates()) || upCoords.equals(child.getCoordinates()); + final var hasDown = this.elements.containsKey(downCoords) || downCoords.equals(parent.getCoordinates()) || downCoords.equals(child.getCoordinates()); + final var hasRight = this.elements.containsKey(rightCoords) || rightCoords.equals(parent.getCoordinates()) || rightCoords.equals(child.getCoordinates()); + final var hasLeft = this.elements.containsKey(leftCoords) || leftCoords.equals(parent.getCoordinates()) || leftCoords.equals(child.getCoordinates()); + + if ((hasUp || hasDown) && !hasLeft && !hasRight) return PathShape.UP; + else if ((hasRight || hasLeft) && !hasUp && !hasDown) return PathShape.RIGHT; + else if (hasUp && hasRight) return PathShape.UP_RIGHT; + else if (hasUp && hasLeft) return PathShape.UP_LEFT; + else if (hasDown && hasRight) return PathShape.DOWN_RIGHT; + else if (hasDown && hasLeft) return PathShape.DOWN_LEFT; + + return PathShape.DEFAULT; + } + + public boolean isSymmetrical() { + return reciprocal; + } + + @NotNull + public Set getElements() { + return elements.keySet(); + } + + @Override + public String toString() { + return "ParentInformation{" + + "child=" + child + + ", parent=" + parent + + ", type=" + type + + ", minLevel=" + minLevel + + ", elements=" + elements + + '}'; + } + + @NotNull + public SkillTreeNode getParent() { + return parent; + } + + @NotNull + public SkillTreeNode getChild() { + return child; } @NotNull @@ -25,8 +140,12 @@ public class ParentInformation { return type; } + /** + * @return Minimum level of parent node required + * for the child node to be reachable. + */ public int getLevel() { - return level; + return minLevel; } @Override @@ -34,11 +153,33 @@ public class ParentInformation { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ParentInformation that = (ParentInformation) o; - return Objects.equals(node, that.node) && type == that.type; + // Big Hypothesis = there are NO two edges with the same child, parent and type. + return Objects.equals(child, that.child) && Objects.equals(parent, that.parent) && type == that.type; } @Override public int hashCode() { - return Objects.hash(node, type); + return Objects.hash(child, parent, type); + } + + @NotNull + public static ParentInformation fromConfig(@NotNull SkillTreeNode child, + @NotNull SkillTreeNode parent, + @NotNull ParentType parentType, + @NotNull Object configObject) { + Validate.notNull(configObject, "Cannot load parent info from null object"); + + // From simple int, no path. + if (configObject instanceof Integer) { + // TODO try to infer paths from 'paths' config for backwards compatibility + return new ParentInformation(child, parent, parentType, false, (Integer) configObject); + } + + // From config section + if (configObject instanceof ConfigurationSection) { + return new ParentInformation(child, parent, parentType, (ConfigurationSection) configObject); + } + + throw new IllegalArgumentException("Cannot load parent from " + configObject.getClass().getName()); } } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreeNode.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreeNode.java index f6ef7b16..c397416d 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreeNode.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreeNode.java @@ -1,8 +1,10 @@ package net.Indyuce.mmocore.skilltree; import io.lumine.mythic.lib.MythicLib; +import io.lumine.mythic.lib.UtilityMethods; import io.lumine.mythic.lib.gui.editable.placeholder.Placeholders; import io.lumine.mythic.lib.gui.util.IconOptions; +import io.lumine.mythic.lib.util.PostLoadAction; import io.lumine.mythic.lib.util.lang3.Validate; import net.Indyuce.mmocore.MMOCore; import net.Indyuce.mmocore.api.player.PlayerData; @@ -23,13 +25,14 @@ import java.util.*; import java.util.stream.Collectors; // We must use generics to get the type of the corresponding tree + public class SkillTreeNode implements ExperienceObject { private final SkillTree tree; private final String name, id; private final String permissionRequired; private final int pointConsumption; private final DisplayMap icons; - private final IntegerCoordinates coordinates; + private final IntCoords coordinates; private final int maxLevel, maxChildren; private final ExperienceTable experienceTable; private final List children = new ArrayList<>(); @@ -38,11 +41,42 @@ public class SkillTreeNode implements ExperienceObject { private boolean root; - public SkillTreeNode(SkillTree tree, ConfigurationSection config) { + private final PostLoadAction postLoadAction = new PostLoadAction(config -> { + + // Load children + // Requires other nodes to be loaded first + loadRelatives(true, config); + + // Load parents. Both work, one way or the other + // Requires other nodes to be loaded first + loadRelatives(false, config); + }); + + private void loadRelatives(boolean nodeIsParent, @NotNull ConfigurationSection config) { + final var configPath = nodeIsParent ? "children" : "parents"; + + if (config.isConfigurationSection(configPath)) + for (var parentTypeRaw : config.getConfigurationSection(configPath).getKeys(false)) { + final var section = config.getConfigurationSection(configPath + "." + parentTypeRaw); + Validate.notNull(section, "Could not read " + configPath + " of type '" + parentTypeRaw + "'"); + final ParentType parentType = UtilityMethods.prettyValueOf(ParentType::valueOf, parentTypeRaw, "No parent type called '%s'"); + + for (var relativeId : section.getKeys(false)) { + final var relative = SkillTreeNode.this.tree.getNode(relativeId); + final var child = nodeIsParent ? relative : this; + final var parent = nodeIsParent ? this : relative; + child.addParent(ParentInformation.fromConfig(child, parent, parentType, section.get(relativeId))); + } + } + } + + public SkillTreeNode(@NotNull SkillTree tree, @NotNull ConfigurationSection config) { Validate.notNull(config, "Config cannot be null"); this.id = config.getName(); this.tree = tree; + postLoadAction.cacheConfig(config); + // Load icons for node states this.icons = DisplayMap.from(config.getConfigurationSection("display")); @@ -70,7 +104,7 @@ public class SkillTreeNode implements ExperienceObject { Validate.isTrue(maxLevel > 0, "Max level must be positive"); maxChildren = config.getInt("max-children", 0); Validate.isTrue(maxChildren >= 0, "Max children must positive or zero"); - coordinates = IntegerCoordinates.from(config.get("coordinates")); + coordinates = IntCoords.from(config.get("coordinates")); } public SkillTree getTree() { @@ -81,12 +115,23 @@ public class SkillTreeNode implements ExperienceObject { return root; } - public void addParent(@NotNull SkillTreeNode parent, @NotNull ParentType parentType, int requiredLevel) { - parents.add(new ParentInformation(parent, parentType, requiredLevel)); + @NotNull + public PostLoadAction getPostLoadAction() { + return postLoadAction; } - public void addChild(@NotNull SkillTreeNode child, @NotNull ParentType parentType, int requiredLevel) { - children.add(new ParentInformation(child, parentType, requiredLevel)); + /** + * Registers this relation both as a parent and child relation in the right + * registers of the parent and child nodes. + *

+ * Note that the {@link #children} and {@link #parents} maps are only + * modified through this method. + */ + public void addParent(@NotNull ParentInformation parentInfo) { + Validate.isTrue(parentInfo.getChild().equals(this), "#addParent(..) must be called on child node"); + + parents.add(parentInfo); + parentInfo.getParent().children.add(parentInfo); } public void setRoot() { @@ -97,24 +142,9 @@ public class SkillTreeNode implements ExperienceObject { return pointConsumption; } - public int getParentNeededLevel(SkillTreeNode parent) { - for (ParentInformation entry : parents) - if (entry.getNode().equals(parent)) - return entry.getLevel(); - throw new RuntimeException("Could not find parent " + parent.getId() + " for node " + id); - } - - @Deprecated - public int getParentNeededLevel(SkillTreeNode parent, ParentType parentType) { - for (ParentInformation entry : parents) - if (entry.getNode().equals(parent) && entry.getType() == parentType) - return entry.getLevel(); - return 0; - } - public boolean hasParent(SkillTreeNode parent) { - for (ParentInformation entry : parents) - if (entry.getNode() == parent) return true; + for (var edge : parents) + if (edge.getParent().equals(parent)) return true; return false; } @@ -135,12 +165,6 @@ public class SkillTreeNode implements ExperienceObject { return parents; } - @NotNull - @Deprecated - public List getParents(ParentType parentType) { - return parents.stream().filter(integer -> integer.getType() == parentType).map(ParentInformation::getNode).collect(Collectors.toList()); - } - @NotNull public List getChildren() { return children; @@ -155,7 +179,7 @@ public class SkillTreeNode implements ExperienceObject { /** * @return Full node identifier, containing both the node identifier AND - * the skill tree identifier, like "combat_extra_strength" + * the skill tree identifier, like "combat_extra_strength" */ @NotNull public String getFullId() { @@ -168,7 +192,7 @@ public class SkillTreeNode implements ExperienceObject { } @NotNull - public IntegerCoordinates getCoordinates() { + public IntCoords getCoordinates() { return coordinates; } @@ -195,30 +219,6 @@ public class SkillTreeNode implements ExperienceObject { return experienceTable != null; } - public NodeShape getNodeType() { - boolean up = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX(), coordinates.getY() - 1)); - boolean down = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX(), coordinates.getY() + 1)); - boolean right = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX() + 1, coordinates.getY())); - boolean left = tree.isPathOrNode(new IntegerCoordinates(coordinates.getX() - 1, coordinates.getY())); - - if (up && right && down && left) return NodeShape.UP_RIGHT_DOWN_LEFT; - else if (up && right && down) return NodeShape.UP_RIGHT_DOWN; - else if (up && right && left) return NodeShape.UP_RIGHT_LEFT; - else if (up && down && left) return NodeShape.UP_DOWN_LEFT; - else if (down && right && left) return NodeShape.DOWN_RIGHT_LEFT; - else if (up && right) return NodeShape.UP_RIGHT; - else if (up && down) return NodeShape.UP_DOWN; - else if (up && left) return NodeShape.UP_LEFT; - else if (down && right) return NodeShape.DOWN_RIGHT; - else if (down && left) return NodeShape.DOWN_LEFT; - else if (right && left) return NodeShape.RIGHT_LEFT; - else if (up) return NodeShape.UP; - else if (down) return NodeShape.DOWN; - else if (right) return NodeShape.RIGHT; - else if (left) return NodeShape.LEFT; - return NodeShape.NO_PATH; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -277,6 +277,26 @@ public class SkillTreeNode implements ExperienceObject { //region Deprecated + @Deprecated + public int getParentNeededLevel(SkillTreeNode parent) { + for (var edge : parents) + if (edge.getParent().equals(parent)) return edge.getLevel(); + throw new RuntimeException("Could not find parent " + parent.getId() + " for node " + id); + } + + @NotNull + @Deprecated + public List getParents(ParentType parentType) { + return parents.stream().filter(integer -> integer.getType() == parentType).map(ParentInformation::getParent).collect(Collectors.toList()); + } + + @Deprecated + public int getParentNeededLevel(SkillTreeNode parent, ParentType parentType) { + for (var edge : parents) + if (edge.getParent().equals(parent) && edge.getType() == parentType) return edge.getLevel(); + return 0; + } + @Deprecated public boolean hasIcon(NodeState status) { return getIcon(status) != null; diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreePath.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreePath.java deleted file mode 100644 index 53165356..00000000 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/SkillTreePath.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.Indyuce.mmocore.skilltree; - -import net.Indyuce.mmocore.api.player.PlayerData; -import net.Indyuce.mmocore.skilltree.display.PathShape; -import net.Indyuce.mmocore.skilltree.tree.SkillTree; - -public class SkillTreePath { - private final SkillTree tree; - private final IntegerCoordinates coordinates; - private final SkillTreeNode from; - private final SkillTreeNode to; - - public SkillTreePath(SkillTree tree, IntegerCoordinates coordinates, SkillTreeNode from, SkillTreeNode skillTreeNode) { - this.tree = tree; - this.coordinates = coordinates; - this.from = from; - to = skillTreeNode; - } - - /** - * Defines the status of a path between two nodes, which is determined - * by the pair of states of the two nodes. - */ - public PathState getStatus(PlayerData playerData) { - var from = playerData.getNodeState(this.from); - var to = playerData.getNodeState(this.to); - - // Either one is fully locked => gray out path - if (from == NodeState.FULLY_LOCKED || to == NodeState.FULLY_LOCKED) return PathState.FULLY_LOCKED; - - // Both are unlocked => path is taken, unlocked - if (from.isUnlocked() && to.isUnlocked()) return PathState.UNLOCKED; - - // One of them is unlocked, other one is unlockable => path is not taken yet, but can be - if ((from == NodeState.UNLOCKABLE && to.isUnlocked()) || (from.isUnlocked() && to == NodeState.UNLOCKABLE)) - return PathState.UNLOCKABLE; - - // Otherwise, locked path - return PathState.LOCKED; - } - - public PathShape getPathType() { - IntegerCoordinates upCoor = new IntegerCoordinates(coordinates.getX(), coordinates.getY() - 1); - IntegerCoordinates downCoor = new IntegerCoordinates(coordinates.getX(), coordinates.getY() + 1); - IntegerCoordinates rightCoor = new IntegerCoordinates(coordinates.getX() + 1, coordinates.getY()); - IntegerCoordinates leftCoor = new IntegerCoordinates(coordinates.getX() - 1, coordinates.getY()); - boolean hasUp = tree.isPath(upCoor) || upCoor.equals(from.getCoordinates()) || upCoor.equals(to.getCoordinates()); - boolean hasDown = tree.isPath(downCoor) || downCoor.equals(from.getCoordinates()) || downCoor.equals(to.getCoordinates()); - boolean hasRight = tree.isPath(rightCoor) || rightCoor.equals(from.getCoordinates()) || rightCoor.equals(to.getCoordinates()); - boolean hasLeft = tree.isPath(leftCoor) || leftCoor.equals(from.getCoordinates()) || leftCoor.equals(to.getCoordinates()); - - if ((hasUp || hasDown) && !hasLeft && !hasRight) { - return PathShape.UP; - } else if ((hasRight || hasLeft) && !hasUp && !hasDown) { - return PathShape.RIGHT; - } else if (hasUp && hasRight) { - return PathShape.UP_RIGHT; - } else if (hasUp && hasLeft) { - return PathShape.UP_LEFT; - } else if (hasDown && hasRight) { - return PathShape.DOWN_RIGHT; - } else if (hasDown && hasLeft) { - return PathShape.DOWN_LEFT; - } - return PathShape.DEFAULT; - } -} diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/DisplayMap.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/DisplayMap.java index 7bdc173a..e00773ee 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/DisplayMap.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/DisplayMap.java @@ -3,7 +3,6 @@ package net.Indyuce.mmocore.skilltree.display; import io.lumine.mythic.lib.UtilityMethods; import io.lumine.mythic.lib.gui.util.IconOptions; import net.Indyuce.mmocore.skilltree.NodeState; -import net.Indyuce.mmocore.skilltree.PathState; import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathDisplayInfo.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathDisplayInfo.java index be3b641e..7543ad84 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathDisplayInfo.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathDisplayInfo.java @@ -1,6 +1,5 @@ package net.Indyuce.mmocore.skilltree.display; -import net.Indyuce.mmocore.skilltree.PathState; import org.jetbrains.annotations.NotNull; import java.util.Objects; @@ -15,7 +14,7 @@ public class PathDisplayInfo { } @NotNull - public PathState getStatus() { + public PathState getState() { return state; } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/PathState.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathState.java similarity index 80% rename from MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/PathState.java rename to MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathState.java index 3c5c743e..38bd13e2 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/PathState.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/display/PathState.java @@ -1,10 +1,5 @@ -package net.Indyuce.mmocore.skilltree; +package net.Indyuce.mmocore.skilltree.display; -import net.Indyuce.mmocore.api.player.PlayerData; - -/** - * @see SkillTreePath#getStatus(PlayerData) - */ public enum PathState { /** diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/ProximitySkillTree.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/ProximitySkillTree.java index ad550afd..884d19b1 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/ProximitySkillTree.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/ProximitySkillTree.java @@ -1,6 +1,7 @@ package net.Indyuce.mmocore.skilltree.tree; -import net.Indyuce.mmocore.skilltree.IntegerCoordinates; +import net.Indyuce.mmocore.skilltree.IntCoords; +import net.Indyuce.mmocore.skilltree.ParentInformation; import net.Indyuce.mmocore.skilltree.ParentType; import net.Indyuce.mmocore.skilltree.SkillTreeNode; import org.bukkit.configuration.ConfigurationSection; @@ -10,20 +11,20 @@ public class ProximitySkillTree extends SkillTree { super(config); // Neighbors are marked as soft parents - for (SkillTreeNode node : nodes.values()) - for (IntegerCoordinates relative : RELATIVES) { + for (var node : nodes.values()) + for (var relative : RELATIVES) { final SkillTreeNode neighbor = this.getNodeOrNull(node.getCoordinates().add(relative)); if (neighbor != null) { - node.addParent(neighbor, ParentType.SOFT, 1); - neighbor.addChild(node, ParentType.SOFT, 1); + final var parentInfo = new ParentInformation(node, neighbor, ParentType.SOFT, true,1); + node.addParent(parentInfo); } } } - private static final IntegerCoordinates[] RELATIVES = { - new IntegerCoordinates(1, 0), - new IntegerCoordinates(-1, 0), - new IntegerCoordinates(0, 1), - new IntegerCoordinates(0, -1) + private static final IntCoords[] RELATIVES = { + new IntCoords(1, 0), + new IntCoords(-1, 0), + new IntCoords(0, 1), + new IntCoords(0, -1) }; } diff --git a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/SkillTree.java b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/SkillTree.java index bb0f38a6..246ae63e 100644 --- a/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/SkillTree.java +++ b/MMOCore-API/src/main/java/net/Indyuce/mmocore/skilltree/tree/SkillTree.java @@ -9,6 +9,8 @@ import net.Indyuce.mmocore.api.player.PlayerData; import net.Indyuce.mmocore.manager.registry.RegisteredObject; import net.Indyuce.mmocore.skilltree.*; import net.Indyuce.mmocore.skilltree.display.DisplayMap; +import net.Indyuce.mmocore.skilltree.display.NodeShape; +import net.Indyuce.mmocore.skilltree.display.PathState; import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; import org.jetbrains.annotations.NotNull; @@ -41,9 +43,6 @@ public abstract class SkillTree implements RegisteredObject { protected final List roots = new ArrayList<>(); protected final DisplayMap icons; - protected final Map coordNodes = new HashMap<>(); - protected final Map coordPaths = new HashMap<>(); - public SkillTree(@NotNull ConfigurationSection config) { this.id = Objects.requireNonNull(config.getString("id"), "Could not find skill tree id"); this.name = MythicLib.plugin.parseColors(Objects.requireNonNull(config.getString("name"), "Could not find skill tree name")); @@ -59,54 +58,28 @@ public abstract class SkillTree implements RegisteredObject { ConfigurationSection section = config.getConfigurationSection("nodes." + key); SkillTreeNode node = new SkillTreeNode(this, section); nodes.put(node.getId(), node); - coordNodes.put(node.getCoordinates(), node); + nodeByCoordinate.put(node.getCoordinates(), node); if (node.isRoot()) roots.add(node); - } catch (Exception e) { - MMOCore.log("Couldn't load skill tree node " + id + "." + key + ": " + e.getMessage()); + } catch (Exception exception) { + MMOCore.log(Level.WARNING, "Couldn't load skill tree node " + id + "." + key + ": " + exception.getMessage()); } - // Load paths - for (String from : config.getConfigurationSection("nodes").getKeys(false)) { - ConfigurationSection section = config.getConfigurationSection("nodes." + from); - if (section.contains("paths")) { - for (String to : section.getConfigurationSection("paths").getKeys(false)) { - SkillTreeNode node1 = nodes.get(to); - if (node1 == null) { - MMOCore.log("Couldn't find node " + to + " for path in node " + from + "."); - continue; - } - for (String pathKey : section.getConfigurationSection("paths." + to).getKeys(false)) { - IntegerCoordinates coordinates = IntegerCoordinates.from(section.get("paths." + to + "." + pathKey)); - coordPaths.put(coordinates, new SkillTreePath(this, coordinates, nodes.get(from), node1)); - } - } + // Post load all nodes + for (var node : nodes.values()) + try { + node.getPostLoadAction().performAction(); + node.getParents().forEach(parentInfo -> parentInfo.getElements().forEach(coords -> this.pathByCoordinate.put(coords, parentInfo))); + } catch (Exception exception) { + MMOCore.log(Level.WARNING, "Couldn't post-load skill tree node " + id + "." + node.getId() + ": " + exception.getMessage()); + exception.printStackTrace(); // TODO remove } - } + + // Resolve node shapes + resolveNodeShapes(); // Load icons this.icons = DisplayMap.from(config.getConfigurationSection("display")); - - // Setup children and parents for each node - for (SkillTreeNode node : nodes.values()) - try { - if (config.isConfigurationSection("nodes." + node.getId() + ".parents")) - for (String key : config.getConfigurationSection("nodes." + node.getId() + ".parents").getKeys(false)) { - final ConfigurationSection section = config.getConfigurationSection("nodes." + node.getId() + ".parents." + key); - if (section != null) { - final ParentType parentType = ParentType.valueOf(UtilityMethods.enumName(key)); - - for (String parentId : section.getKeys(false)) { - final SkillTreeNode parent = getNode(parentId); - final int level = section.getInt(parentId); - node.addParent(parent, parentType, level); - parent.addChild(node, parentType, level); - } - } - } - } catch (RuntimeException exception) { - MMOCore.plugin.getLogger().log(Level.WARNING, "Could not load parents of skill tree node '" + node.getId() + "': " + exception.getMessage()); - } } public List getLore() { @@ -130,21 +103,46 @@ public abstract class SkillTree implements RegisteredObject { return roots; } + //region Resolving states and shapes + + private final Map nodeShapes = new HashMap<>(); + + @NotNull + public NodeShape getNodeShape(@NotNull SkillTreeNode node) { + return Objects.requireNonNull(nodeShapes.get(node), "Missing node shape"); + } + + private void resolveNodeShapes() { + for (var node : nodes.values()) nodeShapes.put(node, resolveNodeShape(node)); + } + /** - * TODO Write documentation + * Resets all states saved for the nodes and edges of + * this skill tree and recomputes them from scratch solely + * based on the nodes unlocked (node level map) by the + * player and points spent in each of them. + *

* TODO Use some collection and progressively filter out the nodes to avoid useless iterations *

* Let: + * - A the number of path elements (not edges) * - V denote the number of nodes in the skill tree - * - P the number of parents any node, has at most + * - P the number of parents any node has, at most * - C the number of children any node has, at most *

- * This algorithm runs in O(V * P * C) + * This algorithm runs in O(V * P * C). In Minecraft nodes + * can't have more than 4 children or parents which makes + * it O(V). + * + * @author jules */ - public void setupNodeStates(@NotNull PlayerData playerData) { + public void resolveStates(@NotNull PlayerData playerData) { + playerData.clearStates(this); + resolveNodeStates(playerData); + resolvePathStates(playerData); + } - // Reinitialization - playerData.clearNodeStates(this); + private void resolveNodeStates(@NotNull PlayerData playerData) { // If the player has already spent the maximum amount of points in this skill tree. final boolean skillTreeLocked = playerData.getPointsSpent(this) >= this.maxPointSpent; @@ -153,25 +151,27 @@ public abstract class SkillTree implements RegisteredObject { // PASS 1 // // Initialization. Mark all nodes either locked or unlocked - for (SkillTreeNode node : nodes.values()) - playerData.setNodeState(node, playerData.getNodeLevel(node) > 0 ? NodeState.UNLOCKED : lockState); + // Mark nodes as "Maxed out" if maximum level is reached. + for (var node : nodes.values()) { + final var nodeLevel = playerData.getNodeLevel(node); + playerData.setNodeState(node, nodeLevel == 0 ? lockState : nodeLevel == node.getMaxLevel() ? NodeState.MAXED_OUT : NodeState.UNLOCKED); + } if (skillTreeLocked) return; // PASS 2 // - // Apply basic unreachability rules in O(V * [C + P]) + // Apply level 1-unreachability rules in O(V * [C + P]) // It has to differ from pass 1 because it uses results from pass 1. - final Stack unreachable = new Stack<>(); - final Set updated = new HashSet<>(); + final var unreachable = new Stack(); - for (SkillTreeNode node : nodes.values()) { + for (var node : nodes.values()) { // INCOMPATIBILITY RULES // // Any node with an unlocked incompatible parent is made unreachable. - for (ParentInformation parent : node.getParents()) - if (parent.getType() == ParentType.INCOMPATIBLE && playerData.getNodeState(parent.getNode()) == NodeState.UNLOCKED) { + for (var edge : node.getParents()) + if (edge.getType() == ParentType.INCOMPATIBLE && playerData.getNodeState(edge.getParent()).isUnlocked()) { unreachable.add(node); break; } @@ -184,13 +184,14 @@ public abstract class SkillTree implements RegisteredObject { if (maxChildren > 0) { int unlocked = 0; - final List locked = new ArrayList<>(); + final var locked = new ArrayList(); - for (ParentInformation child : node.getChildren()) - switch (playerData.getNodeState(child.getNode())) { + for (var edge : node.getChildren()) + switch (playerData.getNodeState(edge.getChild())) { case LOCKED: - locked.add(child.getNode()); + locked.add(edge.getChild()); break; + case MAXED_OUT: case UNLOCKED: unlocked++; break; @@ -202,17 +203,18 @@ public abstract class SkillTree implements RegisteredObject { // PASS 3 // - // Propagate unreachability in O(V * C * P) + // Propagate level 1-unreachability in O(V * C * P) // Unreachability is transitive, if one node is unreachable, all subsequent // child nodes are all unreachable. + final var unreachableCheck = new HashSet(); while (!unreachable.empty()) { - final SkillTreeNode node = unreachable.pop(); + final var node = unreachable.pop(); - updated.add(node); + unreachableCheck.add(node); playerData.setNodeState(node, NodeState.FULLY_LOCKED); - for (ParentInformation child : node.getChildren()) // Propagate - if (!updated.contains(child.getNode()) && isUnreachable(child.getNode(), playerData)) - unreachable.push(child.getNode()); + for (var edge : node.getChildren()) // Propagate + if (!unreachableCheck.contains(edge.getChild()) && isUnreachable(edge.getChild(), playerData)) + unreachable.push(edge.getChild()); } // PASS 4 @@ -221,7 +223,7 @@ public abstract class SkillTree implements RegisteredObject { // because the distance between the set of all unlocked nodes and the set // of all unlockable nodes is at most 1 (unlockability is not "transitive") pass4: - for (SkillTreeNode node : nodes.values()) { + for (var node : nodes.values()) { if (playerData.getNodeState(node) != NodeState.LOCKED) continue; // ROOT NODES @@ -239,12 +241,12 @@ public abstract class SkillTree implements RegisteredObject { // One soft parent of any node must be unlocked for the node to be unlockable. boolean soft = false, hasSoft = false; - for (ParentInformation parent : node.getParents()) { - if (parent.getType() == ParentType.STRONG && playerData.getNodeLevel(parent.getNode()) < parent.getLevel()) + for (var edge : node.getParents()) { + if (edge.getType() == ParentType.STRONG && playerData.getNodeLevel(edge.getParent()) < edge.getLevel()) continue pass4; // Keep the node locked - else if (!soft && parent.getType() == ParentType.SOFT) { + else if (!soft && edge.getType() == ParentType.SOFT) { hasSoft = true; - if (playerData.getNodeLevel(parent.getNode()) >= parent.getLevel()) + if (playerData.getNodeLevel(edge.getParent()) >= edge.getLevel()) soft = true; // Cannot continue, must check for other strong parents } } @@ -252,16 +254,78 @@ public abstract class SkillTree implements RegisteredObject { // At least one soft parent! if (!hasSoft || soft) playerData.setNodeState(node, NodeState.UNLOCKABLE); } + } + + private void resolvePathStates(@NotNull PlayerData playerData) { // PASS 5 // - // I'm not sure this is the best place to do that but it works just fine. - // Mark unlocked nodes as maxed out if maximum number of points spent - for (var node : nodes.values()) - if (playerData.getNodeState(node) == NodeState.UNLOCKED && playerData.getNodeLevel(node) >= node.getMaxLevel()) - playerData.setNodeState(node, NodeState.MAXED_OUT); + // Resolve path states. Iterate through parents of nodes (children would work too) + // TODO merge this with steps 1 to 4 (my brain is fried atm) + for (var node : nodeByCoordinate.values()) + for (var edge : node.getParents()) + playerData.setPathState(edge, resolvePathState(playerData, edge)); } + @NotNull + private PathState resolvePathState(@NotNull PlayerData playerData, @NotNull ParentInformation edge) { + + final var from = playerData.getNodeState(edge.getParent()); + final var to = playerData.getNodeState(edge.getChild()); + final var symm = edge.isSymmetrical(); + + // Gray out path if target is fully locked + // If symmetrical, check again after permutation + if (to == NodeState.FULLY_LOCKED || (symm && from == NodeState.FULLY_LOCKED)) + return PathState.FULLY_LOCKED; + + // Both are unlocked => path is taken, unlocked + // Symmetric relation so 'symm' does not matter + if (from.isUnlocked() && to.isUnlocked()) return PathState.UNLOCKED; + + // If source is unlocked and target unlockable => UNLOCKABLE + // If symmetrical, check again after permutation + if ((from.isUnlocked() && to == NodeState.UNLOCKABLE) || (symm && (to.isUnlocked() && from == NodeState.UNLOCKABLE))) + return PathState.UNLOCKABLE; + + // Locked path by default + return PathState.LOCKED; + } + + @NotNull + private NodeShape resolveNodeShape(@NotNull SkillTreeNode node) { + final var coordinates = node.getCoordinates(); + + final var upCoords = new IntCoords(coordinates.getX(), coordinates.getY() - 1); + final var downCoords = new IntCoords(coordinates.getX(), coordinates.getY() + 1); + final var rightCoords = new IntCoords(coordinates.getX() + 1, coordinates.getY()); + final var leftCoords = new IntCoords(coordinates.getX() - 1, coordinates.getY()); + + final var up = this.nodeByCoordinate.containsKey(upCoords) || this.pathByCoordinate.containsKey(upCoords); + final var down = this.nodeByCoordinate.containsKey(downCoords) || this.pathByCoordinate.containsKey(downCoords); + final var right = this.nodeByCoordinate.containsKey(rightCoords) || this.pathByCoordinate.containsKey(rightCoords); + final var left = this.nodeByCoordinate.containsKey(leftCoords) || this.pathByCoordinate.containsKey(leftCoords); + + if (up && right && down && left) return NodeShape.UP_RIGHT_DOWN_LEFT; + else if (up && right && down) return NodeShape.UP_RIGHT_DOWN; + else if (up && right && left) return NodeShape.UP_RIGHT_LEFT; + else if (up && down && left) return NodeShape.UP_DOWN_LEFT; + else if (down && right && left) return NodeShape.DOWN_RIGHT_LEFT; + else if (up && right) return NodeShape.UP_RIGHT; + else if (up && down) return NodeShape.UP_DOWN; + else if (up && left) return NodeShape.UP_LEFT; + else if (down && right) return NodeShape.DOWN_RIGHT; + else if (down && left) return NodeShape.DOWN_LEFT; + else if (right && left) return NodeShape.RIGHT_LEFT; + else if (up) return NodeShape.UP; + else if (down) return NodeShape.DOWN; + else if (right) return NodeShape.RIGHT; + else if (left) return NodeShape.LEFT; + return NodeShape.NO_PATH; + } + + //endregion + private boolean isUnreachable(@NotNull SkillTreeNode node, @NotNull PlayerData playerData) { // UNREACHABILITY RULES @@ -271,12 +335,12 @@ public abstract class SkillTree implements RegisteredObject { // This rule is the logical opposite of the reachability rule. boolean soft = false, hasSoft = false; - for (ParentInformation parent : node.getParents()) { - if (parent.getType() == ParentType.STRONG && playerData.getNodeState(parent.getNode()) == NodeState.FULLY_LOCKED) + for (var edge : node.getParents()) { + if (edge.getType() == ParentType.STRONG && playerData.getNodeState(edge.getParent()) == NodeState.FULLY_LOCKED) return true; - else if (!soft && parent.getType() == ParentType.SOFT) { + else if (!soft && edge.getType() == ParentType.SOFT) { hasSoft = true; - if (playerData.getNodeState(parent.getNode()) != NodeState.FULLY_LOCKED) + if (playerData.getNodeState(edge.getParent()) != NodeState.FULLY_LOCKED) soft = true; // Cannot continue, must check for other strong parents } } @@ -284,18 +348,7 @@ public abstract class SkillTree implements RegisteredObject { return hasSoft && !soft; } - public boolean isNode(@NotNull IntegerCoordinates coordinates) { - return coordNodes.containsKey(coordinates); - } - - public boolean isPath(@NotNull IntegerCoordinates coordinates) { - return coordPaths.containsKey(coordinates); - } - - public boolean isPathOrNode(IntegerCoordinates coordinates) { - return isNode(coordinates) || isPath(coordinates); - } - + @NotNull public Material getItem() { return item; } @@ -305,29 +358,38 @@ public abstract class SkillTree implements RegisteredObject { return id; } + @NotNull public String getName() { return name; } + @NotNull public Collection getNodes() { return nodes.values(); } + //region Geometry + + protected final Map nodeByCoordinate = new HashMap<>(); + protected final Map pathByCoordinate = new HashMap<>(); + @NotNull - public SkillTreeNode getNode(@NotNull IntegerCoordinates coords) { - return Objects.requireNonNull(coordNodes.get(coords), "Could not find node in tree '" + id + "' with coordinates '" + coords + "'"); + public SkillTreeNode getNode(@NotNull IntCoords coords) { + return Objects.requireNonNull(nodeByCoordinate.get(coords), "Could not find node in tree '" + id + "' with coordinates '" + coords + "'"); } @Nullable - public SkillTreeNode getNodeOrNull(@NotNull IntegerCoordinates coords) { - return coordNodes.get(coords); + public SkillTreeNode getNodeOrNull(@NotNull IntCoords coords) { + return nodeByCoordinate.get(coords); } - @NotNull - public SkillTreePath getPath(@NotNull IntegerCoordinates coords) { - return Objects.requireNonNull(coordPaths.get(coords), "Could not find path in tree '" + id + "' with coordinates '" + coords + "'"); + @Nullable + public ParentInformation getPath(@NotNull IntCoords coords) { + return pathByCoordinate.get(coords); } + //endregion + @NotNull public SkillTreeNode getNode(@NotNull String name) { return Objects.requireNonNull(nodes.get(name), "Could not find node in tree '" + id + "' with name '" + name + "'"); @@ -338,10 +400,6 @@ public abstract class SkillTree implements RegisteredObject { return icons; } - public boolean isNode(String name) { - return nodes.containsKey(name); - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -357,6 +415,28 @@ public abstract class SkillTree implements RegisteredObject { //region Deprecated + @Deprecated + public boolean isNode(@NotNull IntCoords coordinates) { + // TODO remove usage + return nodeByCoordinate.containsKey(coordinates); + } + + @Deprecated + public boolean isPath(@NotNull IntCoords coordinates) { + // TODO remove usage + return pathByCoordinate.containsKey(coordinates); + } + + @Deprecated + public boolean isNode(String name) { + return nodes.containsKey(name); + } + + @Deprecated + public boolean isPathOrNode(IntCoords coordinates) { + return isNode(coordinates) || isPath(coordinates); + } + @Deprecated public static SkillTree loadSkillTree(ConfigurationSection config) { return MMOCore.plugin.skillTreeManager.loadSkillTree(config);